diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 53d616a5..2743a2a3 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -19,24 +19,24 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 17 - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'adopt' - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - uses: gradle/gradle-build-action@v2 - with: - arguments: test -DrunExecutionTest=false # ignore execution test due to local file accessing might causing unexpected issues - - name: Publish Test Report - uses: mikepenz/action-junit-report@v2 - if: always() # always run even if the previous step fails - with: - report_paths: '**/build/test-results/test/TEST-*.xml' - - name: Upload test report - uses: codecov/codecov-action@v2 - with: - files: "build/coverage/coverage.xml" + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + uses: gradle/gradle-build-action@v2 + with: + arguments: test -DrunExecutionTest=false # ignore execution test due to local file accessing might causing unexpected issues + - name: Publish Test Report + uses: mikepenz/action-junit-report@v2 + if: always() # always run even if the previous step fails + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + - name: Upload test report + uses: codecov/codecov-action@v2 + with: + files: "build/coverage/coverage.xml" diff --git a/.github/workflows/weekly_release.yml b/.github/workflows/weekly_release.yml index a803b9e9..730b5c91 100644 --- a/.github/workflows/weekly_release.yml +++ b/.github/workflows/weekly_release.yml @@ -5,11 +5,11 @@ # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle -name: CASC Weekly Release +name: CASC Weekly Release on: schedule: - - cron: "0 0 * * 1" + - cron: "0 0 * * 1" workflow_dispatch: jobs: @@ -18,45 +18,45 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Get current date - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Set up JDK 17 - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'adopt' - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build - - name: Jar built files with Gradle - run: ./gradlew jar - - name: Create release - uses: actions/create-release@v1 - id: create_release - with: - draft: false - prerelease: false - release_name: ${{ steps.version.outputs.version }} - tag_name: ${{ steps.date.outputs.date }} - env: - GITHUB_TOKEN: ${{ github.token }} - - name: Upload Jar file to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./build/libs/CASC-0.0.1.jar - asset_name: CASC-Weekly-${{ steps.date.outputs.date }}.jar - asset_content_type: application/zip - - name: Publish Jar file - uses: actions/upload-artifact@v2.3.1 - with: - # Artifact name - name: CASC-Weekly-${{ steps.date.outputs.date }}.jar - # A file, directory or wildcard pattern that describes what to upload - path: ./build/libs/CASC-0.0.1.jar - # The desired behavior if no files are found using the provided path. + - uses: actions/checkout@v2 + - name: Get current date + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - name: Jar built files with Gradle + run: ./gradlew jar + - name: Create release + uses: actions/create-release@v1 + id: create_release + with: + draft: false + prerelease: false + release_name: ${{ steps.version.outputs.version }} + tag_name: ${{ steps.date.outputs.date }} + env: + GITHUB_TOKEN: ${{ github.token }} + - name: Upload Jar file to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./build/libs/CASC-0.0.1.jar + asset_name: CASC-Weekly-${{ steps.date.outputs.date }}.jar + asset_content_type: application/zip + - name: Publish Jar file + uses: actions/upload-artifact@v2.3.1 + with: + # Artifact name + name: CASC-Weekly-${{ steps.date.outputs.date }}.jar + # A file, directory or wildcard pattern that describes what to upload + path: ./build/libs/CASC-0.0.1.jar + # The desired behavior if no files are found using the provided path. diff --git a/TODO.md b/TODO.md index 171eefe7..5be86201 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,11 @@ ## TODO List + ``There is no precedence for this list, all tasks are currently invloving.`` To see release-based TODOs, checkout GitHub repository's project + ### Category + #### Language Spec ##### TO BE INCLUDED IN UNIT TEST @@ -30,18 +33,26 @@ To see release-based TODOs, checkout GitHub repository's project ##### IMPLEMENTATION IN PROGRESS - [ ] loops -- - [x] for loop (Java loop)[1](#f1) -- - [ ] range for loop[2](#f2) -- - [ ] while loop -- - [ ] do-while loop +- + - [x] for loop (Java loop)[1](#f1) +- + - [ ] range for loop[2](#f2) +- + - [ ] while loop +- + - [ ] do-while loop - [ ] label - [ ] generic - [ ] .class - [ ] inheritance -- - [x] class -- - [ ] abstract class -- - [ ] interface -- - [x] `super` call +- + - [x] class +- + - [ ] abstract class +- + - [ ] interface +- + - [x] `super` call ### Compiler Unit @@ -49,5 +60,6 @@ To see release-based TODOs, checkout GitHub repository's project - [ ] more customizable compilation arguments(?) #### FOOTNOTES + 1. Most likely will remove after range for loop is done. Due to syntax parsing collision. [↩](#a1) 2. Requires inheritance fully supported. [↩](#a2) diff --git a/src/main/kotlin/org/casc/lang/asm/BytecodeClassLoader.kt b/src/main/kotlin/org/casc/lang/asm/BytecodeClassLoader.kt index a06b6f09..457485f8 100644 --- a/src/main/kotlin/org/casc/lang/asm/BytecodeClassLoader.kt +++ b/src/main/kotlin/org/casc/lang/asm/BytecodeClassLoader.kt @@ -1,8 +1,7 @@ package org.casc.lang.asm -import java.net.URLClassLoader -import java.lang.ClassNotFoundException import java.net.URL +import java.net.URLClassLoader class BytecodeClassLoader(urls: Array?, parent: ClassLoader?) : URLClassLoader(urls, parent) { fun defineClass(className: String?, bytecode: ByteArray): Class<*> { diff --git a/src/main/kotlin/org/casc/lang/asm/CommonClassWriter.kt b/src/main/kotlin/org/casc/lang/asm/CommonClassWriter.kt index 3a3b8050..5d6f589c 100644 --- a/src/main/kotlin/org/casc/lang/asm/CommonClassWriter.kt +++ b/src/main/kotlin/org/casc/lang/asm/CommonClassWriter.kt @@ -6,4 +6,10 @@ class CommonClassWriter(flags: Int, private val classLoader: ClassLoader) : Clas override fun getClassLoader(): ClassLoader { return classLoader } + + override fun getCommonSuperClass(type1: String?, type2: String?): String { + // search common type from Table + + return super.getCommonSuperClass(type1, type2) + } } \ No newline at end of file diff --git a/src/main/kotlin/org/casc/lang/ast/ClassInstance.kt b/src/main/kotlin/org/casc/lang/ast/ClassInstance.kt index 028b2b0a..99b769c3 100644 --- a/src/main/kotlin/org/casc/lang/ast/ClassInstance.kt +++ b/src/main/kotlin/org/casc/lang/ast/ClassInstance.kt @@ -1,6 +1,5 @@ package org.casc.lang.ast -import org.casc.lang.table.HasFlag import org.casc.lang.table.Reference import org.casc.lang.utils.getOrElse import org.objectweb.asm.Opcodes @@ -8,22 +7,17 @@ import org.objectweb.asm.Opcodes data class ClassInstance( override val packageReference: Reference?, val accessorToken: Token?, - val abstrToken: Token?, // Unused + val abstrToken: Token?, val mutKeyword: Token?, - val classKeyword: Token?, + val classKeyword: Token, override val typeReference: Reference, override val fields: List, override val accessor: Accessor = Accessor.fromString(accessorToken?.literal) -) : TypeInstance(), HasFlag { +) : TypeInstance() { override val flag: Int by lazy { var flag = Opcodes.ACC_SUPER flag += accessor.access - flag += abstrToken.getOrElse(Opcodes.ACC_ABSTRACT) - flag += mutKeyword.getOrElse(0, Opcodes.ACC_FINAL) + flag += abstrToken.getOrElse(Opcodes.ACC_ABSTRACT, mutKeyword.getOrElse(0, Opcodes.ACC_FINAL)) flag } - - val parentClassReference: Reference by lazy { - impl?.parentClassReference ?: Reference.OBJECT_TYPE_REFERENCE - } } diff --git a/src/main/kotlin/org/casc/lang/ast/CompanionBlock.kt b/src/main/kotlin/org/casc/lang/ast/CompanionBlock.kt new file mode 100644 index 00000000..40be370f --- /dev/null +++ b/src/main/kotlin/org/casc/lang/ast/CompanionBlock.kt @@ -0,0 +1,3 @@ +package org.casc.lang.ast + +data class CompanionBlock(val compKeyword: Token, val statements: List) diff --git a/src/main/kotlin/org/casc/lang/ast/Constructor.kt b/src/main/kotlin/org/casc/lang/ast/Constructor.kt index 4b2dafdd..79b8e142 100644 --- a/src/main/kotlin/org/casc/lang/ast/Constructor.kt +++ b/src/main/kotlin/org/casc/lang/ast/Constructor.kt @@ -6,7 +6,7 @@ data class Constructor( val ownerReference: Reference?, var parentReference: Reference?, val accessorToken: Token?, - val newKeyword: Token?, + val newKeyword: Token, val parameters: List, val statements: List, val superKeyword: Token?, @@ -34,6 +34,7 @@ data class Constructor( ownerType, companion = true, mutable = false, + abstract = false, accessor, "", parameterTypes.mapNotNull { it }, diff --git a/src/main/kotlin/org/casc/lang/ast/Expression.kt b/src/main/kotlin/org/casc/lang/ast/Expression.kt index bea6f05a..1fe6758e 100644 --- a/src/main/kotlin/org/casc/lang/ast/Expression.kt +++ b/src/main/kotlin/org/casc/lang/ast/Expression.kt @@ -5,7 +5,6 @@ import org.casc.lang.table.PrimitiveType import org.casc.lang.table.Reference import org.casc.lang.table.Type import org.objectweb.asm.Opcodes -import org.objectweb.asm.TypeReference sealed class Expression { companion object { diff --git a/src/main/kotlin/org/casc/lang/ast/File.kt b/src/main/kotlin/org/casc/lang/ast/File.kt index 67cf48c1..1ebede00 100644 --- a/src/main/kotlin/org/casc/lang/ast/File.kt +++ b/src/main/kotlin/org/casc/lang/ast/File.kt @@ -2,7 +2,12 @@ package org.casc.lang.ast import org.casc.lang.table.Reference -data class File(val path: String, val relativeFilePath: String, val usages: List, var typeInstance: TypeInstance) { +data class File( + val path: String, + val relativeFilePath: String, + val usages: List, + var typeInstance: TypeInstance +) { val fileName: String by lazy { relativeFilePath.split('\\', '/').last() } diff --git a/src/main/kotlin/org/casc/lang/ast/Function.kt b/src/main/kotlin/org/casc/lang/ast/Function.kt index 2e0eaa85..2807aa44 100644 --- a/src/main/kotlin/org/casc/lang/ast/Function.kt +++ b/src/main/kotlin/org/casc/lang/ast/Function.kt @@ -3,7 +3,6 @@ package org.casc.lang.ast import org.casc.lang.table.* import org.casc.lang.utils.getOrElse import org.objectweb.asm.Opcodes -import java.lang.reflect.Modifier data class Function( val ownerReference: Reference?, @@ -11,6 +10,7 @@ data class Function( val ovrdKeyword: Token?, val abstrKeyword: Token?, val mutKeyword: Token?, + val fnKeyword: Token, val selfKeyword: Token?, // determine whether function is companion val name: Token?, val parameters: List, @@ -26,7 +26,7 @@ data class Function( parameterTypes?.fold("") { s, type -> s + type?.descriptor } - })${returnType?.descriptor}" + })${returnType?.descriptor ?: "V"}" override val flag: Int by lazy { var flag = accessor.access flag += selfKeyword.getOrElse(0, Opcodes.ACC_STATIC) @@ -35,6 +35,7 @@ data class Function( flag += mutKeyword.getOrElse(0, Opcodes.ACC_FINAL) flag } + var abstract: Boolean = abstrKeyword != null override fun asSignature() = FunctionSignature( @@ -42,6 +43,7 @@ data class Function( ownerType, selfKeyword == null, mutKeyword != null, + abstrKeyword != null, accessor, name?.literal ?: "", parameterTypes!!.mapNotNull { it }, diff --git a/src/main/kotlin/org/casc/lang/ast/Impl.kt b/src/main/kotlin/org/casc/lang/ast/Impl.kt index 74eac57e..e33982d5 100644 --- a/src/main/kotlin/org/casc/lang/ast/Impl.kt +++ b/src/main/kotlin/org/casc/lang/ast/Impl.kt @@ -5,7 +5,7 @@ import org.casc.lang.table.Reference data class Impl( val implKeyword: Token, val parentClassReference: Reference?, - var companionBlock: List, + var companionBlock: CompanionBlock?, var constructors: List, var functions: List ) diff --git a/src/main/kotlin/org/casc/lang/ast/Statement.kt b/src/main/kotlin/org/casc/lang/ast/Statement.kt index cbd4c281..d16e3c57 100644 --- a/src/main/kotlin/org/casc/lang/ast/Statement.kt +++ b/src/main/kotlin/org/casc/lang/ast/Statement.kt @@ -11,7 +11,8 @@ sealed class Statement { val expressions: List, var indexes: MutableList = mutableListOf(), override val pos: Position? = Position.fromMultipleAndExtend( - *variables.firstOrNull()?.toList()?.map { it?.pos }?.toTypedArray() ?: arrayOf()) + *variables.firstOrNull()?.toList()?.map { it?.pos }?.toTypedArray() ?: arrayOf() + ) ?.extend(expressions.lastOrNull()?.pos) ) : Statement() diff --git a/src/main/kotlin/org/casc/lang/ast/TraitImpl.kt b/src/main/kotlin/org/casc/lang/ast/TraitImpl.kt index 456d125f..16847014 100644 --- a/src/main/kotlin/org/casc/lang/ast/TraitImpl.kt +++ b/src/main/kotlin/org/casc/lang/ast/TraitImpl.kt @@ -1,3 +1,5 @@ package org.casc.lang.ast -data class TraitImpl(val implKeyword: Token, val functions: List) +import org.casc.lang.table.Reference + +data class TraitImpl(val implKeyword: Token, val implementedTraitReference: Reference, val functions: List) diff --git a/src/main/kotlin/org/casc/lang/ast/TraitInstance.kt b/src/main/kotlin/org/casc/lang/ast/TraitInstance.kt index 418a44ce..0fca0e2a 100644 --- a/src/main/kotlin/org/casc/lang/ast/TraitInstance.kt +++ b/src/main/kotlin/org/casc/lang/ast/TraitInstance.kt @@ -1,6 +1,5 @@ package org.casc.lang.ast -import org.casc.lang.table.HasFlag import org.casc.lang.table.Reference import org.objectweb.asm.Opcodes @@ -11,7 +10,7 @@ data class TraitInstance( override val typeReference: Reference, override val fields: List, override val accessor: Accessor = Accessor.fromString(accessorToken?.literal) -) : TypeInstance(), HasFlag { +) : TypeInstance() { override val flag: Int by lazy { accessor.access + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE } diff --git a/src/main/kotlin/org/casc/lang/ast/TypeInstance.kt b/src/main/kotlin/org/casc/lang/ast/TypeInstance.kt index 60540c77..baa8c464 100644 --- a/src/main/kotlin/org/casc/lang/ast/TypeInstance.kt +++ b/src/main/kotlin/org/casc/lang/ast/TypeInstance.kt @@ -1,8 +1,9 @@ package org.casc.lang.ast +import org.casc.lang.table.HasFlag import org.casc.lang.table.Reference -sealed class TypeInstance { +sealed class TypeInstance : HasFlag { abstract val packageReference: Reference? abstract val typeReference: Reference abstract val fields: List @@ -14,4 +15,12 @@ sealed class TypeInstance { val reference: Reference by lazy { Reference(packageReference?.fullQualifiedPath, typeReference.fullQualifiedPath) } + + val parentClassReference: Reference by lazy { + impl?.parentClassReference ?: Reference.OBJECT_TYPE_REFERENCE + } + + val traitClassReferences: Array by lazy { + traitImpls?.map(TraitImpl::implementedTraitReference)?.toTypedArray() ?: arrayOf() + } } \ No newline at end of file diff --git a/src/main/kotlin/org/casc/lang/checker/Checker.kt b/src/main/kotlin/org/casc/lang/checker/Checker.kt index 016cb5d2..dfa28cca 100644 --- a/src/main/kotlin/org/casc/lang/checker/Checker.kt +++ b/src/main/kotlin/org/casc/lang/checker/Checker.kt @@ -60,18 +60,18 @@ class Checker(private val preference: AbstractPreference) { if (parentClassType != null) { if (parentClassType !is ClassType) { reports += Error( - clazz.parentClassReference.pos ?: clazz.classKeyword?.pos, + clazz.parentClassReference.pos, "Cannot inherit from non-class type ${parentClassType.asCASCStyle()}" ) } else if (parentClassType != ClassType.OBJECT_TYPE && !parentClassType.mutable) { reports += Error( - clazz.parentClassReference.pos ?: clazz.classKeyword?.pos, + clazz.parentClassReference.pos, "Cannot inherit from final class ${parentClassType.asCASCStyle()}", "Add `mut` to class ${parentClassType.asCASCStyle()}" ) } else if (parentClassType.type(preference)?.let { Modifier.isFinal(it.modifiers) } == true) { reports += Error( - clazz.parentClassReference.pos ?: clazz.classKeyword?.pos, + clazz.parentClassReference.pos, "Cannot inherit from final class ${parentClassType.asCASCStyle()}", "Add `mut` to class ${parentClassType.asCASCStyle()}" ) @@ -96,6 +96,14 @@ class Checker(private val preference: AbstractPreference) { } } + if (clazz.traitImpls != null) { + clazz.traitImpls!!.forEach { + it.functions.forEach { function -> + checkFunction(function, classScope, it.implementedTraitReference) + } + } + } + return classScope } @@ -133,6 +141,14 @@ class Checker(private val preference: AbstractPreference) { } } + if (trait.traitImpls != null) { + trait.traitImpls!!.forEach { + it.functions.forEach { function -> + checkFunction(function, traitScope, it.implementedTraitReference) + } + } + } + return traitScope } @@ -198,7 +214,9 @@ class Checker(private val preference: AbstractPreference) { if (topScope.usages.find { usage -> usage.fullQualifiedPath == reference.fullQualifiedPath } != null) { // Using an already used package or class reports += Warning( - reference.pos, "${reference.asCASCStyle()} is already used in this context", "Consider removing this usage" + reference.pos, + "${reference.asCASCStyle()} is already used in this context", + "Consider removing this usage" ) } @@ -246,7 +264,7 @@ class Checker(private val preference: AbstractPreference) { if (clazz.impl != null) { val impl = clazz.impl!! - impl.companionBlock.forEach { + impl.companionBlock?.statements?.forEach { checkStatement(it, companionBlockScope, PrimitiveType.Unit) } @@ -267,6 +285,23 @@ class Checker(private val preference: AbstractPreference) { } } } + + if (clazz.traitImpls != null) { + for (traitImpl in clazz.traitImpls!!) { + traitImpl.functions.forEach { + if (it.statements != null) { + checkFunctionBody(it, Scope(classScope, isCompScope = it.selfKeyword == null)) + + if (!checkControlFlow(it.statements, it.returnType)) { + // Not all code path returns value + reports += Error( + it.name?.pos, "Not all code path returns value" + ) + } + } + } + } + } } private fun checkTrait(file: File, trait: TraitInstance, traitScope: Scope) { @@ -281,7 +316,7 @@ class Checker(private val preference: AbstractPreference) { if (trait.impl != null) { val impl = trait.impl!! - impl.companionBlock.forEach { + impl.companionBlock?.statements?.forEach { checkStatement(it, companionBlockScope, PrimitiveType.Unit) } @@ -298,6 +333,23 @@ class Checker(private val preference: AbstractPreference) { } } } + + if (trait.traitImpls != null) { + for (traitImpl in trait.traitImpls!!) { + traitImpl.functions.forEach { + if (it.statements != null) { + checkFunctionBody(it, Scope(traitScope, isCompScope = it.selfKeyword == null)) + + if (!checkControlFlow(it.statements, it.returnType)) { + // Not all code path returns value + reports += Error( + it.name?.pos, "Not all code path returns value" + ) + } + } + } + } + } } private fun checkField(field: Field, scope: Scope): Field { @@ -364,12 +416,19 @@ class Checker(private val preference: AbstractPreference) { } } - if (validationPass) scope.registerSignature(constructor) + if (validationPass && !scope.registerSignature(constructor)) { + // Duplicate constructors + reports += Error( + constructor.newKeyword.pos, + "Constructor `new`(${DisplayFactory.getParametersTypePretty(constructor.parameters)}) has already declared in same context", + "Try modify parameters' type" + ) + } return constructor } - private fun checkFunction(function: Function, scope: Scope): Function { + private fun checkFunction(function: Function, scope: Scope, traitReference: Reference? = null): Function { val localScope = Scope(scope) checkIdentifierIsKeyword(function.name?.literal, function.name?.pos) @@ -420,7 +479,7 @@ class Checker(private val preference: AbstractPreference) { // Check is overriding parent function val parentFunction = - scope.findSignature(scope.parentClassPath, function.name!!.literal, function.parameterTypes ?: listOf()) + scope.findSignature(traitReference ?: scope.parentClassPath, function.name!!.literal, function.parameterTypes ?: listOf()) if (parentFunction != null) { if (function.ovrdKeyword == null) { @@ -462,7 +521,14 @@ class Checker(private val preference: AbstractPreference) { ) } - if (validationPass) scope.registerSignature(function) + if (validationPass && !scope.registerSignature(function)) { + // Duplicate functions + reports += Error( + function.name.pos, + "Function ${function.name.literal}(${DisplayFactory.getParametersTypePretty(function.parameters)}) has already declared in same context", + "Try rename this function or modify parameters' type" + ) + } return function } @@ -479,7 +545,7 @@ class Checker(private val preference: AbstractPreference) { if (superCallSignature == null) { // No super call match reports += Error( - constructor.newKeyword?.pos, "Cannot find matched super call `super`(${ + constructor.newKeyword.pos, "Cannot find matched super call `super`(${ constructor.parentConstructorArgumentsTypes.mapNotNull { it?.typeName }.joinToString() })" ) @@ -493,7 +559,7 @@ class Checker(private val preference: AbstractPreference) { if (thisCallSignature == null) { // No super call match reports += Error( - constructor.newKeyword?.pos, "Cannot find matched super call `this`(${ + constructor.newKeyword.pos, "Cannot find matched super call `this`(${ constructor.parentConstructorArgumentsTypes.mapNotNull { it?.typeName }.joinToString() })" ) @@ -502,7 +568,7 @@ class Checker(private val preference: AbstractPreference) { if (scope.parentClassPath != Reference.OBJECT_TYPE_REFERENCE && scope.parentClassPath != null) { // Requires `super` call reports += Error( - constructor.newKeyword?.pos, + constructor.newKeyword.pos, "Class ${scope.typeReference} extends class ${scope.parentClassPath} but doesn't `super` any parent class' constructor", "Add `super` call after constructor declaration" ) @@ -511,6 +577,7 @@ class Checker(private val preference: AbstractPreference) { ClassType.OBJECT_TYPE, companion = true, mutable = false, + abstract = false, Accessor.Pub, "", listOf(), @@ -648,10 +715,12 @@ class Checker(private val preference: AbstractPreference) { } } is BlockStatement -> { + val blockScope = Scope(scope) + statement.statements.forEachIndexed { i, it -> checkStatement( it!!, - Scope(scope), + blockScope, returnType, if (i == statement.statements.size - 1) retainLastExpression else false ) @@ -960,7 +1029,7 @@ class Checker(private val preference: AbstractPreference) { // Check function call expression's context, e.g companion context val ownerReference = expression.ownerReference ?: previousType?.getReference() ?: scope.typeReference val functionSignature = scope.findSignature( - ownerReference, expression.name!!.literal, argumentTypes + ownerReference, expression.name!!.literal, argumentTypes, allowAbstract = false ) if (functionSignature == null) { @@ -1316,17 +1385,21 @@ class Checker(private val preference: AbstractPreference) { ) } else expression.type = targetType } else if (originalType is ArrayType) { - if (targetType is PrimitiveType) { - reports += Error( - expression.targetTypeReference?.pos, - "Cannot cast array type `${originalType.asCASCStyle()}` into primitive type `${targetType.asCASCStyle()}`" - ) - } else if (targetType is ClassType) { - reports += Error( - expression.targetTypeReference?.pos, - "Cannot cast array type `${originalType.asCASCStyle()}` into class type `${targetType.asCASCStyle()}`" - ) - } else expression.type = targetType + when (targetType) { + is PrimitiveType -> { + reports += Error( + expression.targetTypeReference?.pos, + "Cannot cast array type `${originalType.asCASCStyle()}` into primitive type `${targetType.asCASCStyle()}`" + ) + } + is ClassType -> { + reports += Error( + expression.targetTypeReference?.pos, + "Cannot cast array type `${originalType.asCASCStyle()}` into class type `${targetType.asCASCStyle()}`" + ) + } + else -> expression.type = targetType + } } expression.type diff --git a/src/main/kotlin/org/casc/lang/compilation/AbstractPreference.kt b/src/main/kotlin/org/casc/lang/compilation/AbstractPreference.kt index 69aed56b..e7e4122c 100644 --- a/src/main/kotlin/org/casc/lang/compilation/AbstractPreference.kt +++ b/src/main/kotlin/org/casc/lang/compilation/AbstractPreference.kt @@ -2,11 +2,13 @@ package org.casc.lang.compilation import org.casc.lang.asm.BytecodeClassLoader import java.io.File -import java.net.URLClassLoader abstract class AbstractPreference { abstract var enableColor: Boolean - open var classLoader: BytecodeClassLoader = BytecodeClassLoader(arrayOf(File(System.getProperty("user.dir"), "out").toURI().toURL()), ClassLoader.getSystemClassLoader()) + open var classLoader: BytecodeClassLoader = BytecodeClassLoader( + arrayOf(File(System.getProperty("user.dir"), "out").toURI().toURL()), + ClassLoader.getSystemClassLoader() + ) abstract var sourceFile: File? abstract var enableTiming: Boolean var outputDir: File = File(System.getProperty("user.dir"), "out") diff --git a/src/main/kotlin/org/casc/lang/compilation/Compilation.kt b/src/main/kotlin/org/casc/lang/compilation/Compilation.kt index b21aee13..58d9520d 100644 --- a/src/main/kotlin/org/casc/lang/compilation/Compilation.kt +++ b/src/main/kotlin/org/casc/lang/compilation/Compilation.kt @@ -2,16 +2,16 @@ package org.casc.lang.compilation import com.diogonunes.jcolor.AnsiFormat import com.diogonunes.jcolor.Attribute -import org.casc.lang.ast.ClassInstance -import org.casc.lang.ast.File -import org.casc.lang.ast.Token +import org.casc.lang.ast.* import org.casc.lang.checker.Checker import org.casc.lang.emitter.Emitter import org.casc.lang.lexer.Lexer import org.casc.lang.parser.Parser +import org.casc.lang.table.Reference import org.casc.lang.table.Scope import org.casc.lang.table.Table import java.io.BufferedReader +import java.util.* import kotlin.time.DurationUnit import kotlin.time.ExperimentalTime @@ -25,7 +25,7 @@ class Compilation(private val preference: AbstractPreference) { fun compile() { var panic = false - val sourceFile = preference.sourceFile!!.absoluteFile!! + val sourceFile = preference.sourceFile!!.absoluteFile measureTime("Compilation") { if (sourceFile.isDirectory) { @@ -41,7 +41,7 @@ class Compilation(private val preference: AbstractPreference) { val source = cascFile.readLines() val relativeFilePath = cascFile.toRelativeString(sourceFile) val compilationUnit = - CompilationFileUnit(cascFile.name, source, relativeFilePath, cascFile.path) + CompilationFileUnit(cascFile.name, source, cascFile.path, relativeFilePath) // Unit I: Lexer /** @@ -56,6 +56,8 @@ class Compilation(private val preference: AbstractPreference) { } } + compilationUnits.sortBy { it.relativePath } + compilationUnits.printReports() if (compilationUnits.anyError()) { @@ -87,7 +89,7 @@ class Compilation(private val preference: AbstractPreference) { return@measureTime } - measureTime("Check (Prelude)") { + measureTime("Check (Prelude)") checkPrelude@{ // Caches class for dummy type checking, used in declaration checking Table.cachedClasses += compilationUnits.map { it.file.typeInstance.reference to it.file } @@ -108,48 +110,87 @@ class Compilation(private val preference: AbstractPreference) { compilationUnit.scope = classScope } - compilationUnits.printReports() - if (compilationUnits.anyError()) { panic = true - return@measureTime + return@checkPrelude } - Table.cachedClasses.clear() - - // Define temporary class through bytecode for ASM library to process (See [getClassLoader][org.casc.lang.asm.CommonClassWriter]) - val creationQueue = LinkedHashSet() + val traitQueue = mutableListOf() + val classQueue = mutableListOf() val declarations = compilationUnits.map(CompilationFileUnit::file) - fun addToQueue(file: File) { - val typeInstance = file.typeInstance + for ((file, compilationUnit) in declarations.zip(compilationUnits)) { + // Check parent class has no cyclic inheritance + val (_, _, _, typeInstance) = file - if (typeInstance is ClassInstance && typeInstance.impl?.parentClassReference != null) { - val parentClazzFile = - declarations.find { it.typeInstance.reference.fullQualifiedPath == typeInstance.impl!!.parentClassReference!!.fullQualifiedPath } - if (parentClazzFile != null) - addToQueue(parentClazzFile) + when (typeInstance) { + is ClassInstance -> + classQueue += typeInstance.reference + is TraitInstance -> + traitQueue += typeInstance.reference + } + + + if (typeInstance is ClassInstance) { + val parentClassReferenceSet = hashSetOf() + var currentTypeInstance = typeInstance + + while (currentTypeInstance.impl != null && currentTypeInstance.impl!!.parentClassReference != null) { + currentTypeInstance = + Table.findTypeInstance(currentTypeInstance.impl!!.parentClassReference!!) + ?: break + + if (!parentClassReferenceSet.add(currentTypeInstance.typeReference)) { + // Cyclic inheritance + compilationUnit.reports += Error( + typeInstance.impl!!.parentClassReference!!.pos, + "Circular inheritance is forbidden", + "Class ${typeInstance.reference.asCASCStyle()} inherits class ${currentTypeInstance.reference.asCASCStyle()} but class ${currentTypeInstance.reference.asCASCStyle()} also inherits class ${typeInstance.reference.asCASCStyle()}" + ) + break + } else classQueue.add(0, currentTypeInstance.reference) + } } if (typeInstance.traitImpls != null) { - // TODO: implement traitImpls precaching + val currentTraitQueue = + LinkedList(typeInstance.traitImpls!!.map(TraitImpl::implementedTraitReference)) + + while (currentTraitQueue.isNotEmpty()) { + val trait = currentTraitQueue.remove() + traitQueue.add(0, trait) + Table.findFile(trait)!!.typeInstance.traitImpls?.map(TraitImpl::implementedTraitReference) + ?.let(currentTraitQueue::addAll) + } } + } - creationQueue.add(file.typeInstance.reference.fullQualifiedPath) + if (compilationUnits.anyError()) { + panic = true + return@checkPrelude } - declarations.forEach(::addToQueue) + // Define temporary class through bytecode for ASM library to process (See [getClassLoader][org.casc.lang.asm.CommonClassWriter]) + for (reference in traitQueue.distinct()) { + val cachedFile = Table.findFile(reference)!! + val bytecode = Emitter(preference, true).emit(cachedFile) - for (name in creationQueue) { - val cachedFile = - declarations.find { it.typeInstance.reference.fullQualifiedPath == name }!! + preference.classLoader.defineClass(reference.fullQualifiedPath, bytecode) + } + for (reference in classQueue.distinct()) { + val cachedFile = Table.findFile(reference)!! val bytecode = Emitter(preference, true).emit(cachedFile) - preference.classLoader.defineClass(name, bytecode) + preference.classLoader.defineClass(reference.fullQualifiedPath, bytecode) } + } - Table.cachedClasses += declarations.map { it.typeInstance.reference to it } + compilationUnits.printReports() + + if (compilationUnits.anyError()) { + panic = true + return@measureTime } measureTime("Check (Main)") { diff --git a/src/main/kotlin/org/casc/lang/compilation/CompilationFileUnit.kt b/src/main/kotlin/org/casc/lang/compilation/CompilationFileUnit.kt index a2354909..297819ff 100644 --- a/src/main/kotlin/org/casc/lang/compilation/CompilationFileUnit.kt +++ b/src/main/kotlin/org/casc/lang/compilation/CompilationFileUnit.kt @@ -4,7 +4,12 @@ import org.casc.lang.ast.File import org.casc.lang.ast.Token import org.casc.lang.table.Scope -data class CompilationFileUnit(val fileName: String, val source: List, val filePath: String, val relativePath: String) { +data class CompilationFileUnit( + val fileName: String, + val source: List, + val filePath: String, + val relativePath: String +) { var tokens: List = listOf() var reports: List = mutableListOf() lateinit var file: File diff --git a/src/main/kotlin/org/casc/lang/compilation/GlobalPreference.kt b/src/main/kotlin/org/casc/lang/compilation/GlobalPreference.kt index 27f0b5db..1a4286bb 100644 --- a/src/main/kotlin/org/casc/lang/compilation/GlobalPreference.kt +++ b/src/main/kotlin/org/casc/lang/compilation/GlobalPreference.kt @@ -1,8 +1,6 @@ package org.casc.lang.compilation -import org.casc.lang.asm.BytecodeClassLoader import java.io.File -import java.net.URLClassLoader object GlobalPreference : AbstractPreference() { override var enableColor: Boolean = true diff --git a/src/main/kotlin/org/casc/lang/compilation/Report.kt b/src/main/kotlin/org/casc/lang/compilation/Report.kt index 4d6ca93e..f7b3aaaa 100644 --- a/src/main/kotlin/org/casc/lang/compilation/Report.kt +++ b/src/main/kotlin/org/casc/lang/compilation/Report.kt @@ -110,18 +110,18 @@ sealed class Report { } ) else "^".repeat(end - start) - if (hint != null) { - finalMessage += if (preference.enableColor) Ansi.colorize( + finalMessage += if (hint != null) { + if (preference.enableColor) Ansi.colorize( "= hint: $hint\n", reportAttribute[when (this) { is Warning -> 0 is Error -> 1 }] ) else "= hint: $hint\n" } else { - finalMessage += "\n" + "\n" } - if (lineNumber < source.lastIndex) { + if (lineNumber <= source.lastIndex) { finalMessage += "${ if (preference.enableColor) Ansi.colorize( "${lineNumber + 1} |", diff --git a/src/main/kotlin/org/casc/lang/emitter/Emitter.kt b/src/main/kotlin/org/casc/lang/emitter/Emitter.kt index 855cbd56..ebf24230 100644 --- a/src/main/kotlin/org/casc/lang/emitter/Emitter.kt +++ b/src/main/kotlin/org/casc/lang/emitter/Emitter.kt @@ -10,7 +10,6 @@ import org.objectweb.asm.* import java.lang.invoke.CallSite import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType -import java.util.OptionalDouble import java.io.File as JFile class Emitter(private val preference: AbstractPreference, private val declarationOnly: Boolean) { @@ -18,10 +17,7 @@ class Emitter(private val preference: AbstractPreference, private val declaratio emitFile(file) private fun emitFile(file: File): ByteArray { - val bytecode = when (val typeInstance = file.typeInstance) { - is ClassInstance -> emitClass(typeInstance, file.fileName) - is TraitInstance -> emitTrait(typeInstance, file.fileName) - } + val bytecode = emitTypeInstance(file.typeInstance, file.fileName) val outFile = JFile(preference.outputDir, "/${file.typeInstance.typeReference.className}.class") if (!preference.noEmit) { @@ -37,50 +33,25 @@ class Emitter(private val preference: AbstractPreference, private val declaratio return bytecode } - private fun emitClass(clazz: ClassInstance, sourceFile: String): ByteArray { + private fun emitTypeInstance(typeInstance: TypeInstance, sourceFile: String): ByteArray { val classWriter = CommonClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS, preference.classLoader) classWriter.visit( Opcodes.V1_8, - clazz.flag, - clazz.reference.internalName(), + typeInstance.flag, + typeInstance.reference.internalName(), null, - clazz.impl?.parentClassReference?.fullQualifiedPath ?: Reference.OBJECT_TYPE_REFERENCE.internalName(), - null + typeInstance.parentClassReference.internalName(), + typeInstance.traitClassReferences.map(Reference::internalName).toTypedArray() ) classWriter.visitSource(sourceFile, null) if (!declarationOnly) { - clazz.fields.forEach { - emitField(classWriter, it) - } - - if (clazz.impl != null) { - val impl = clazz.impl!! - - if (impl.companionBlock.isNotEmpty()) { - val methodVisitor = classWriter.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null) - - methodVisitor.visitCode() - - impl.companionBlock.forEach { - emitStatement(methodVisitor, it) - } - - methodVisitor.visitInsn(Opcodes.RETURN) - methodVisitor.visitMaxs(-1, -1) - methodVisitor.visitEnd() - } - - impl.constructors.forEach { - emitConstructor(classWriter, it) - } - - impl.functions.forEach { - emitFunction(classWriter, it) - } + when (typeInstance) { + is ClassInstance -> emitClass(classWriter, typeInstance) + is TraitInstance -> emitTrait(classWriter, typeInstance) } } @@ -89,50 +60,48 @@ class Emitter(private val preference: AbstractPreference, private val declaratio return classWriter.toByteArray() } - private fun emitTrait(trait: TraitInstance, sourceFile: String): ByteArray { - val classWriter = - CommonClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS, preference.classLoader) + private fun emitClass(classWriter: ClassWriter, clazz: ClassInstance) { + clazz.fields.forEach { + emitField(classWriter, it) + } - classWriter.visit( - Opcodes.V1_8, - trait.flag, - trait.reference.internalName(), - null, - trait.impl?.parentClassReference?.fullQualifiedPath ?: Reference.OBJECT_TYPE_REFERENCE.internalName(), - null - ) + clazz.impl?.let { emitImpl(classWriter, it) } + clazz.traitImpls?.let { for (traitImpl in it) for (function in traitImpl.functions) emitFunction(classWriter, function) } + } - classWriter.visitSource(sourceFile, null) + private fun emitTrait(classWriter: ClassWriter, trait: TraitInstance) { + trait.fields.forEach { + emitField(classWriter, it) + } - if (!declarationOnly) { - trait.fields.forEach { - emitField(classWriter, it) - } + trait.impl?.let { emitImpl(classWriter, it) } + trait.traitImpls?.let { for (traitImpl in it) for (function in traitImpl.functions) emitFunction(classWriter, function) } + } - if (trait.impl != null) { - val impl = trait.impl!! + private fun emitImpl(classWriter: ClassWriter, impl: Impl) { + val companionBlock = impl.companionBlock - if (impl.companionBlock.isNotEmpty()) { - val methodVisitor = classWriter.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null) + if (companionBlock != null && companionBlock.statements.isNotEmpty()) { + val methodVisitor = classWriter.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null) - methodVisitor.visitCode() + methodVisitor.visitCode() - impl.companionBlock.forEach { - emitStatement(methodVisitor, it) - } + companionBlock.statements.forEach { + emitStatement(methodVisitor, it) + } - methodVisitor.visitInsn(Opcodes.RETURN) - methodVisitor.visitMaxs(-1, -1) - methodVisitor.visitEnd() - } + methodVisitor.visitInsn(Opcodes.RETURN) + methodVisitor.visitMaxs(-1, -1) + methodVisitor.visitEnd() + } - impl.functions.forEach { - emitFunction(classWriter, it) - } - } + impl.functions.forEach { + emitFunction(classWriter, it) } - return classWriter.toByteArray() + impl.constructors.forEach { + emitConstructor(classWriter, it) + } } private fun emitField(classWriter: ClassWriter, field: Field) { @@ -403,8 +372,8 @@ class Emitter(private val preference: AbstractPreference, private val declaratio // Use INVOKEVIRTUAL instead methodVisitor.visitMethodInsn( when { - expression.superCall -> Opcodes.INVOKESPECIAL functionSignature.ownerType?.isTrait == true -> Opcodes.INVOKEINTERFACE + expression.superCall -> Opcodes.INVOKESPECIAL else -> Opcodes.INVOKEVIRTUAL }, functionSignature.ownerReference.internalName(), diff --git a/src/main/kotlin/org/casc/lang/lexer/Lexer.kt b/src/main/kotlin/org/casc/lang/lexer/Lexer.kt index 1c7bca0c..0902708d 100644 --- a/src/main/kotlin/org/casc/lang/lexer/Lexer.kt +++ b/src/main/kotlin/org/casc/lang/lexer/Lexer.kt @@ -318,7 +318,7 @@ class Lexer(private val preference: AbstractPreference) { for (i in 0 until 4) { val current = peek() - if (current.digitToIntOrNull() != null || current in 'a' .. 'f' || current in 'A'..'F') { + if (current.digitToIntOrNull() != null || current in 'a'..'f' || current in 'A'..'F') { unicodeHexBuilder += peekInc() } else { reports += Error( @@ -418,7 +418,7 @@ class Lexer(private val preference: AbstractPreference) { val current = peek() - if (current.digitToIntOrNull() != null || current in 'a' .. 'f' || current in 'A'..'F') { + if (current.digitToIntOrNull() != null || current in 'a'..'f' || current in 'A'..'F') { unicodeHexBuilder += peekInc() } else if (peek() == '\'') { reports += Error( diff --git a/src/main/kotlin/org/casc/lang/parser/Parser.kt b/src/main/kotlin/org/casc/lang/parser/Parser.kt index 1b47f36c..23f9fb2b 100644 --- a/src/main/kotlin/org/casc/lang/parser/Parser.kt +++ b/src/main/kotlin/org/casc/lang/parser/Parser.kt @@ -19,7 +19,6 @@ class Parser(private val preference: AbstractPreference) { private lateinit var tokens: List fun parse(path: String, relativeFilePath: String, tokens: List): Pair, File?> { - pos = 0 this.tokens = tokens val file = parseFile(path, relativeFilePath) @@ -33,17 +32,20 @@ class Parser(private val preference: AbstractPreference) { private fun hasNext(): Boolean = tokens.size > pos private fun assertUntil(type: TokenType): Token? { - while (hasNext()) { + do { val token = assert(type) if (token != null) return token - } + } while ((hasNext())) return null } private fun assert(type: TokenType): Token? = when { - !hasNext() -> null + !hasNext() -> { + reports.reportUnexpectedToken(type, last()?.pos, "Reached last token") + null + } tokens[pos].type == type -> tokens[pos++] else -> { reports.reportUnexpectedToken(type, tokens[pos++]) @@ -52,21 +54,26 @@ class Parser(private val preference: AbstractPreference) { } private fun assertUntil(predicate: (Token) -> Boolean): Token? { - while (hasNext()) { + do { val token = assert(predicate) if (token != null) return token - } + } while ((hasNext())) return null } private fun assert(predicate: (Token) -> Boolean): Token? = when { - !hasNext() -> null + !hasNext() -> { + reports += Error( + last()?.pos, "Expected token but got nothing", "Reached last token" + ) + null + } predicate(tokens[pos]) -> tokens[pos++] else -> { reports += Error( - tokens[pos].pos, "Unexpected token ${tokens[pos++].type}, expected predicate $predicate" + tokens[pos].pos, "Unexpected token ${tokens[pos++].type}" ) null } @@ -129,7 +136,7 @@ class Parser(private val preference: AbstractPreference) { val usages = mutableListOf() val typeInstances = mutableMapOf() val majorImpls = mutableMapOf() - val traitImpls = mutableMapOf>() + val traitImpls = mutableMapOf>() while (hasNext()) { if (peekIf(Token::isUseKeyword)) { @@ -225,10 +232,79 @@ class Parser(private val preference: AbstractPreference) { val ownerReference = parseTypeSymbol() if (peekIf(Token::isForKeyword)) { - // TODO + consume() // `for` keyword + + val implementedTraitReference = parseTypeSymbol() + + var functions: List? = null + + if (peekIf(TokenType.OpenBrace)) { + consume() // open brace + + val (fns, ctors, compBlock) = parseFunctions(ownerReference, null) + + if (ctors.isNotEmpty()) { + // Constructor declarations in trait implementation + for (constructor in ctors) { + reports += Error( + constructor.newKeyword.pos, + "Constructor cannot be declared in trait implementation", + "Remove this constructor declaration" + ) + } + } + + if (compBlock != null) { + // Companion blocks in trait implementation + reports += Error( + compBlock.compKeyword.pos, + "Companion block is forbidden in trait implementation", + "Remove this companion block" + ) + } + + // All functions in trait implementation must be override functions + for (function in fns) { + if (function.ovrdKeyword == null) { + // Function does not attempt to override trait functions + reports += Error( + function.name?.pos, + "Function a" + ) + } + } + + functions = fns + + assertUntil(TokenType.CloseBrace) + } + + val traitImpl = TraitImpl( + implKeyword, + implementedTraitReference, + functions ?: listOf() + ) + + if (!typeInstances.containsKey(ownerReference)) { + // Unknown type implementation + reports += Error( + ownerReference.pos, "Unknown trait implementation for class ${ownerReference.asCASCStyle()}" + ) + } else if (traitImpls[ownerReference]?.find { it.implementedTraitReference == implementedTraitReference } != null) { + // Implementation duplication + reports += Error( + ownerReference.pos, + "Duplicated trait implementation for class ${ownerReference.asCASCStyle()}", + "Consider remove this trait implementation" + ) + } else { + if (traitImpls[ownerReference] == null) + traitImpls[ownerReference] = mutableListOf() + + traitImpls[ownerReference]!! += traitImpl + } } else { // Common implementation - val parentClassReference = if (peekIf(TokenType.Colon)) { consume() // colon parseTypeSymbol() @@ -236,7 +312,7 @@ class Parser(private val preference: AbstractPreference) { var functions: List? = null var constructors: List? = null - var companionBlock: List? = null + var companionBlock: CompanionBlock? = null if (peekIf(TokenType.OpenBrace)) { consume() // open brace @@ -255,7 +331,7 @@ class Parser(private val preference: AbstractPreference) { val impl = Impl( implKeyword, parentClassReference, - companionBlock ?: listOf(), + companionBlock, constructors ?: listOf(), functions ?: listOf() ) @@ -336,7 +412,7 @@ class Parser(private val preference: AbstractPreference) { // Illegal constructor declaration for trait instance's implementation for (constructor in it.constructors) { reports += Error( - constructor.newKeyword?.pos, + constructor.newKeyword.pos, "Trait cannot have constructors", "Remove this constructor declaration" ) @@ -736,16 +812,10 @@ class Parser(private val preference: AbstractPreference) { private fun parseFunctions( classReference: Reference?, parentReference: Reference? - ): Triple, List, List> { - var firstCompKeyword: Token? = null - var companionBlock: List? = null - val functions = object : MutableObjectSet() { - override fun isDuplicate(a: Function, b: Function): Boolean = - a.name?.literal == b.name?.literal && a.parameters == b.parameters - } - val constructors = object : MutableObjectSet() { - override fun isDuplicate(a: Constructor, b: Constructor): Boolean = a.parameters == b.parameters - } + ): Triple, List, CompanionBlock?> { + var companionBlock: CompanionBlock? = null + val functions = mutableListOf() + val constructors = mutableListOf() while (hasNext()) { if (peekIf(TokenType.CloseBrace)) break @@ -777,25 +847,27 @@ class Parser(private val preference: AbstractPreference) { ) } - val compKeyword = next() + val compKeyword = next()!! assertUntil(TokenType.OpenBrace) if (companionBlock != null) { // Companion block already declared reports += Error( - compKeyword?.pos, + compKeyword.pos, "Companion block already declared", "Try merge companion blocks together" ) reports += Error( - firstCompKeyword?.pos, + companionBlock.compKeyword.pos, "Companion block already declared", "Merge to this companion block" ) - } else firstCompKeyword = compKeyword + } + + val statements = parseStatements(true) - companionBlock = parseStatements(true) + companionBlock = CompanionBlock(compKeyword, statements) assertUntil(TokenType.CloseBrace) } else if (peekIf(Token::isNewKeyword)) { @@ -814,7 +886,7 @@ class Parser(private val preference: AbstractPreference) { ) } - val newKeyword = next() // `new` keyword + val newKeyword = next()!! // `new` keyword val (parameterSelfKeyword, parameters) = parseParameters() @@ -864,21 +936,23 @@ class Parser(private val preference: AbstractPreference) { superCallArguments ) - if (!constructors.add(constructor)) { - // Duplicate constructors - reports += Error( - newKeyword?.pos, - "Constructor `new`(${DisplayFactory.getParametersTypePretty(parameters)}) has already declared in same context", - "Try modify parameters' type" - ) - } + constructors += constructor } else if (peekIf(Token::isFnKeyword)) { // Function declaration - consume() // `fn` keyword + val fnKeyword = next()!! val name = assertUntil(TokenType.Identifier) val (parameterSelfKeyword, parameters) = parseParameters() + if (parameterSelfKeyword == null && mutable != null) { + // Companion function cannot be mutable + reports += Error( + mutable.pos, + "Companion function cannot be mutable", + "Remove this `mut` keyword" + ) + } + val returnType = if (peekIf(TokenType.Colon)) { consume() parseComplexTypeSymbol() @@ -898,6 +972,7 @@ class Parser(private val preference: AbstractPreference) { ovrd, abstr, mutable, + fnKeyword, parameterSelfKeyword, name, parameters, @@ -905,18 +980,11 @@ class Parser(private val preference: AbstractPreference) { statements, ) - if (!functions.add(function)) { - // Duplicate functions - reports += Error( - name?.pos, - "Function ${name?.literal}(${DisplayFactory.getParametersTypePretty(parameters)}) has already declared in same context", - "Try rename this function or modify parameters' type" - ) - } + functions += function } else if (peekIf(TokenType.CloseBrace)) break } - return Triple(functions.toList(), constructors.toList(), companionBlock ?: listOf()) + return Triple(functions.toList(), constructors.toList(), companionBlock) } /** diff --git a/src/main/kotlin/org/casc/lang/parser/reporter.kt b/src/main/kotlin/org/casc/lang/parser/reporter.kt index c24a5f68..224881ab 100644 --- a/src/main/kotlin/org/casc/lang/parser/reporter.kt +++ b/src/main/kotlin/org/casc/lang/parser/reporter.kt @@ -1,22 +1,34 @@ package org.casc.lang.parser +import org.casc.lang.ast.Position import org.casc.lang.ast.Token import org.casc.lang.ast.TokenType import org.casc.lang.compilation.Error import org.casc.lang.compilation.Report -internal fun MutableList.reportUnexpectedToken(expected: TokenType, got: Token) = +internal fun MutableList.reportUnexpectedToken(expected: TokenType, got: Token, hint: String? = null) = this.add( Error( got.pos, - "Unexpected token ${got.type}, expected token $expected" + "Unexpected token ${got.type}, expected token $expected", + hint ) ) -internal fun MutableList.reportUnexpectedToken(got: Token) = +internal fun MutableList.reportUnexpectedToken(expected: TokenType, pos: Position?, hint: String? = null) = + this.add( + Error( + pos, + "Expected token $expected, but got nothing", + hint + ) + ) + +internal fun MutableList.reportUnexpectedToken(got: Token, hint: String? = null) = this.add( Error( got.pos, - "Unexpected token ${got.type}" + "Unexpected token ${got.type}", + hint ) ) \ No newline at end of file diff --git a/src/main/kotlin/org/casc/lang/process.kt b/src/main/kotlin/org/casc/lang/process.kt index 05b6fcc7..11a2ff54 100644 --- a/src/main/kotlin/org/casc/lang/process.kt +++ b/src/main/kotlin/org/casc/lang/process.kt @@ -1,6 +1,5 @@ package org.casc.lang -import io.github.classgraph.ClassGraph import org.casc.lang.compilation.AbstractPreference import org.casc.lang.compilation.Compilation import org.casc.lang.compilation.Error @@ -87,7 +86,9 @@ enum class CommandType(val helpMessage: String) { preference.outputDir = outputFolder true } else { - Error("Output path `$outputFolderPath` is not a directory").printReport(preference) + Error("Output path `$outputFolderPath` is not a directory").printReport( + preference + ) false } } else { diff --git a/src/main/kotlin/org/casc/lang/table/ClassType.kt b/src/main/kotlin/org/casc/lang/table/ClassType.kt index 808578d0..0284390a 100644 --- a/src/main/kotlin/org/casc/lang/table/ClassType.kt +++ b/src/main/kotlin/org/casc/lang/table/ClassType.kt @@ -2,7 +2,6 @@ package org.casc.lang.table import org.casc.lang.ast.Accessor import org.casc.lang.compilation.AbstractPreference -import org.casc.lang.compilation.GlobalPreference import org.objectweb.asm.Opcodes import java.lang.reflect.Modifier @@ -28,7 +27,7 @@ data class ClassType( ) override fun type(preference: AbstractPreference): Class<*>? = try { - preference.classLoader?.loadClass(typeName) + preference.classLoader.loadClass(typeName) } catch (e: Exception) { null } diff --git a/src/main/kotlin/org/casc/lang/table/FunctionSignature.kt b/src/main/kotlin/org/casc/lang/table/FunctionSignature.kt index 0b7f5ae8..4cd4777d 100644 --- a/src/main/kotlin/org/casc/lang/table/FunctionSignature.kt +++ b/src/main/kotlin/org/casc/lang/table/FunctionSignature.kt @@ -1,19 +1,35 @@ package org.casc.lang.table import org.casc.lang.ast.Accessor +import org.casc.lang.compilation.AbstractPreference import org.casc.lang.utils.getOrElse import org.objectweb.asm.Opcodes +import java.lang.reflect.Method +import java.lang.reflect.Modifier data class FunctionSignature( val ownerReference: Reference, val ownerType: ClassType?, val companion: Boolean, val mutable: Boolean, + val abstract: Boolean, override val accessor: Accessor, val name: String, val parameters: List, val returnType: Type ) : HasDescriptor, HasFlag, HasAccessor { + constructor(ownerClass: Class<*>, function: Method, preference: AbstractPreference) : this( + Reference(ownerClass), + TypeUtil.asType(ownerClass, preference) as ClassType, + Modifier.isStatic(function.modifiers), + !Modifier.isFinal(function.modifiers), + Modifier.isAbstract(function.modifiers), + Accessor.fromModifier(function.modifiers), + function.name, + function.parameterTypes.map { TypeUtil.asType(it, preference)!! }, + TypeUtil.asType(function.returnType, preference)!! + ) + override val descriptor: String = if (name == "") { // Constructor @@ -24,4 +40,26 @@ data class FunctionSignature( } override val flag: Int = companion.getOrElse(Opcodes.ACC_STATIC) + mutable.getOrElse(0, Opcodes.ACC_FINAL) + accessor.access + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FunctionSignature + + if (ownerReference != other.ownerReference) return false + if (companion != other.companion) return false + if (name != other.name) return false + if (descriptor != other.descriptor) return false + + return true + } + + override fun hashCode(): Int { + var result = ownerReference.hashCode() + result = 31 * result + companion.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + descriptor.hashCode() + return result + } } diff --git a/src/main/kotlin/org/casc/lang/table/HasFlag.kt b/src/main/kotlin/org/casc/lang/table/HasFlag.kt index 125fc09f..5dddb52a 100644 --- a/src/main/kotlin/org/casc/lang/table/HasFlag.kt +++ b/src/main/kotlin/org/casc/lang/table/HasFlag.kt @@ -1,7 +1,6 @@ package org.casc.lang.table import org.casc.lang.ast.Accessor -import org.casc.lang.utils.getOrElse import org.objectweb.asm.Opcodes interface HasFlag { diff --git a/src/main/kotlin/org/casc/lang/table/Scope.kt b/src/main/kotlin/org/casc/lang/table/Scope.kt index 9618b577..557889d7 100644 --- a/src/main/kotlin/org/casc/lang/table/Scope.kt +++ b/src/main/kotlin/org/casc/lang/table/Scope.kt @@ -1,10 +1,15 @@ package org.casc.lang.table import io.github.classgraph.ClassGraph -import org.casc.lang.ast.* +import org.casc.lang.ast.Accessor +import org.casc.lang.ast.ClassInstance +import org.casc.lang.ast.Field +import org.casc.lang.ast.HasSignature import org.casc.lang.compilation.AbstractPreference import org.casc.lang.utils.getOrElse +import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.util.* data class Scope( val preference: AbstractPreference, @@ -51,14 +56,10 @@ data class Scope( init { if (usages.isEmpty()) { // It was just initialized, add classes under package java.lang - val classes = ClassGraph() - .acceptPackagesNonRecursive("java.lang") - .overrideClassLoaders(ClassLoader.getSystemClassLoader()) - .scan() + val classes = ClassGraph().acceptPackagesNonRecursive("java.lang") + .overrideClassLoaders(ClassLoader.getSystemClassLoader()).scan() - - for (classInfo in classes.allStandardClasses) - usages += Reference(classInfo.loadClass().name) + for (classInfo in classes.allStandardClasses) usages += Reference(classInfo.loadClass().name) } } @@ -109,25 +110,30 @@ data class Scope( // Function Signatures // //=======================================// - fun registerSignature(signatureObject: HasSignature) { - signatures += signatureObject.asSignature() - } + fun registerSignature(signatureObject: HasSignature): Boolean = signatures.add(signatureObject.asSignature()) private fun findSignatureInSameType(functionName: String, argumentTypes: List): FunctionSignature? = signatures.find { - it.name == functionName && - it.parameters.size == argumentTypes.size && - argumentTypes - .zip(it.parameters) - .all { (l, r) -> - canCast(l, r) - } + it.name == functionName && it.parameters.size == argumentTypes.size && argumentTypes.zip(it.parameters) + .all { (l, r) -> + canCast(l, r) + } } - fun findSignature(ownerPath: Reference?, functionName: String, argumentTypes: List): FunctionSignature? { + /** + * Searches functions with the following searching order: + * 1. Current scope if: + * - argument `ownerPath` is null + * - argument `ownerPath`'s reference is same as current scope's typeReference + * 2. Type symbol table / JVM Class Loader: + * - always search implemented trait (interface) functions first + * - if previous step does not return available function signature, search parent class + */ + fun findSignature( + ownerPath: Reference?, functionName: String, argumentTypes: List, searchTraitOnly: Boolean = false, allowAbstract: Boolean = true + ): FunctionSignature? { if (ownerPath == null) return findSignatureInSameType( - functionName, - argumentTypes + functionName, argumentTypes ) val reference = findReference(ownerPath) @@ -136,36 +142,48 @@ data class Scope( val typeInstance = Table.findTypeInstance(reference) if (typeInstance != null) { - return if (typeInstance is ClassInstance && functionName == "") { - typeInstance.impl?.constructors?.find { - it.parameterTypes.size == argumentTypes.size && - argumentTypes - .zip(it.parameterTypes) - .all { (l, r) -> - canCast(l, r) - } - }?.asSignature() ?: findSignature(typeInstance.parentClassReference, functionName, argumentTypes) - } else { - // TODO: Find functions from implemented traits - typeInstance.impl?.functions?.find { - it.name?.literal == functionName && - it.parameterTypes?.size == argumentTypes.size && - argumentTypes - .zip(it.parameterTypes!!) - .all { (l, r) -> - canCast(l, r) - } - }?.let(HasSignature::asSignature) ?: when (typeInstance) { - is ClassInstance -> findSignature( - typeInstance.parentClassReference, - functionName, - argumentTypes - ) - is TraitInstance -> null + if (typeInstance is ClassInstance && functionName == "") { + return typeInstance.impl?.constructors?.find { + it.parameterTypes.size == argumentTypes.size && argumentTypes.zip(it.parameterTypes).all { (l, r) -> + canCast(l, r) + } + }?.asSignature() + } + // TODO: Find functions from implemented traits + var functionSignature = typeInstance.impl?.functions?.filter { if (!allowAbstract) !it.abstract else true }?.find { + it.name?.literal == functionName && it.parameterTypes?.size == argumentTypes.size && argumentTypes.zip( + it.parameterTypes!! + ).all { (l, r) -> + canCast(l, r) + } + }?.let(HasSignature::asSignature) + + if (functionSignature != null) return functionSignature + + // Search trait function first + if (typeInstance.traitImpls != null) { + for (traitImpl in typeInstance.traitImpls!!) { + functionSignature = + findSignature(traitImpl.implementedTraitReference, functionName, argumentTypes, allowAbstract) + + if (functionSignature != null) return functionSignature } } + + if (searchTraitOnly) return null + + // Search function in parent class + return if (typeInstance.impl != null) { + findSignature( + typeInstance.impl!!.parentClassReference ?: Reference.OBJECT_TYPE_REFERENCE, + functionName, + argumentTypes, + allowAbstract + ) + } else null } + val ownerType = findType(reference) if (ownerType != null) { @@ -173,66 +191,72 @@ data class Scope( if (functionName == "") { // Constructor - try { - val (ownerClazz, argumentClasses) = retrieveExecutableInfo(ownerType, argTypes) - val constructors = ownerClazz.constructors.filter { it.parameters.size == argumentClasses.size } - - for (constructor in constructors) { - if (constructor.parameterTypes.zip(argumentClasses).all { (l, r) -> l.isAssignableFrom(r) }) { - return FunctionSignature( - Reference(ownerClazz), - TypeUtil.asType(ownerClazz, preference) as ClassType, - companion = true, - mutable = false, - Accessor.fromModifier(constructor.modifiers), - functionName, - constructor.parameterTypes.map { TypeUtil.asType(it, preference)!! }, - ownerType - ) - } + val (ownerClazz, argumentClasses) = retrieveExecutableInfo(ownerType, argTypes) + val constructors = ownerClazz.constructors.filter { it.parameters.size == argumentClasses.size } + + for (constructor in constructors) { + if (constructor.parameterTypes.zip(argumentClasses).all { (l, r) -> l.isAssignableFrom(r) }) { + return FunctionSignature( + Reference(ownerClazz), + TypeUtil.asType(ownerClazz, preference) as ClassType, + companion = true, + mutable = false, + abstract = false, + Accessor.fromModifier(constructor.modifiers), + functionName, + constructor.parameterTypes.map { TypeUtil.asType(it, preference)!! }, + ownerType + ) } - } catch (_: Exception) { } } else { // Function - try { - var (ownerClazz, argumentClasses) = - if (ownerPath == typeReference) retrieveExecutableInfo( - findType(parentClassPath) ?: ClassType(Any::class.java), argTypes - ) - else retrieveExecutableInfo(ownerType, argTypes) - var signature: FunctionSignature? = null - - while (ownerClazz != Any::class.java) { - try { - val functions = - ownerClazz.declaredMethods.filter { it.name == functionName && it.parameters.size == argumentClasses.size } - - for (function in functions) { - if (function.parameterTypes.zip(argumentClasses) - .all { (l, r) -> l.isAssignableFrom(r) } - ) { - signature = FunctionSignature( - Reference(ownerClazz), - TypeUtil.asType(ownerClazz, preference) as ClassType, - Modifier.isStatic(function.modifiers), - Modifier.isFinal(function.modifiers), - Accessor.fromModifier(function.modifiers), - functionName, - function.parameterTypes.map { TypeUtil.asType(it, preference)!! }, - TypeUtil.asType(function.returnType, preference)!! - ) - } - } - - break - } catch (e: Throwable) { - ownerClazz = ownerClazz.superclass + var (ownerClazz, argumentClasses) = if (ownerPath == typeReference) retrieveExecutableInfo( + findType(parentClassPath) ?: ClassType(Any::class.java), argTypes + ) + else retrieveExecutableInfo(ownerType, argTypes) + + var functions = filterFunction(ownerClazz, functionName, argumentClasses, allowAbstract) + + for (function in functions) { + if (isSameFunction(function, argumentClasses)) { + return FunctionSignature(ownerClazz, function, preference) + } + } + + // Search trait function first (BFS) + val traits = LinkedList(ownerClazz.interfaces.toList()) + + while (traits.isNotEmpty()) { + val trait = traits.remove() + functions = filterFunction(trait, functionName, argumentClasses, allowAbstract) + + for (function in functions) { + if (isSameFunction(function, argumentClasses)) { + return FunctionSignature(trait, function, preference) } } - return signature - } catch (_: Throwable) { + traits += trait.interfaces + } + + if (searchTraitOnly) return null + + if (ownerClazz.superclass == null) return null + + ownerClazz = ownerClazz.superclass + + while (true) { + functions = filterFunction(ownerClazz, functionName, argumentClasses, allowAbstract) + + for (function in functions) { + if (isSameFunction(function, argumentClasses)) { + return FunctionSignature(ownerClazz, function, preference) + } + } + + if (ownerClazz.superclass == null) return null + else ownerClazz = ownerClazz.superclass } } } @@ -240,6 +264,24 @@ data class Scope( return null } + private fun filterFunction( + ownerClass: Class<*>, + functionName: String, + argumentClasses: Array>, + allowAbstract: Boolean + ): List = + ownerClass.declaredMethods.filter { it.name == functionName && it.parameters.size == argumentClasses.size && if (!allowAbstract) !Modifier.isAbstract(it.modifiers) else true } + + private fun isSameFunction(function: Method, argumentClasses: Array>): Boolean { + for ((i, parameterType) in function.parameterTypes.withIndex()) { + if (!parameterType.isAssignableFrom(argumentClasses[i])) { + return false + } + } + + return true + } + //=======================================// // Local Variables // //=======================================// @@ -253,11 +295,7 @@ data class Scope( else lastIndex + 1 } val variable = Variable( - mutable, - variableName, - type, - index, - scopeDepth + mutable, variableName, type, index, scopeDepth ) if (variables.contains(variable)) return false @@ -287,17 +325,12 @@ data class Scope( fun findType(reference: Reference?): Type? = when (reference) { null -> null typeReference -> ClassType( - typeReference.fullQualifiedPath, - parentClassPath?.fullQualifiedPath, - accessor, - mutable, - isTrait + typeReference.fullQualifiedPath, parentClassPath?.fullQualifiedPath, accessor, mutable, isTrait ) else -> TypeUtil.asType(findReference(reference), preference) } - fun canCast(from: Type?, to: Type?): Boolean = - TypeUtil.canCast(from, to, preference) + fun canCast(from: Type?, to: Type?): Boolean = TypeUtil.canCast(from, to, preference) /** * Find reference (either shortened, aliased, or full-qualified) from usage, return itself if not found @@ -312,18 +345,15 @@ data class Scope( childReference == null -> false parentReference == childReference -> false childReference == typeReference -> isChildType( - findType(parentReference), - findType(parentClassPath) + findType(parentReference), findType(parentClassPath) ) else -> isChildType(findType(parentReference), findType(childReference)) } - fun isChildType(parentReference: Reference?): Boolean = - isChildType(parentReference, typeReference) + fun isChildType(parentReference: Reference?): Boolean = isChildType(parentReference, typeReference) private fun retrieveExecutableInfo( - ownerType: Type, - argumentTypes: List + ownerType: Type, argumentTypes: List ): Pair, Array>> = ownerType.type(preference)!! to argumentTypes.mapNotNull { it.type(preference) }.toTypedArray() } diff --git a/src/main/kotlin/org/casc/lang/table/Table.kt b/src/main/kotlin/org/casc/lang/table/Table.kt index 290cc86c..2ce0f63d 100644 --- a/src/main/kotlin/org/casc/lang/table/Table.kt +++ b/src/main/kotlin/org/casc/lang/table/Table.kt @@ -19,12 +19,12 @@ object Table { /** * By given full qualified class path (including package path) will return a ClassType if exists or null */ - fun findType(classReference: Reference): ClassType? = - cachedClasses[classReference]?.let { + fun findType(typeReference: Reference): ClassType? = + cachedClasses[typeReference]?.let { val typeInstance = it.typeInstance ClassType( - classReference.fullQualifiedPath, + typeReference.fullQualifiedPath, if (typeInstance is ClassInstance && typeInstance.packageReference != null) typeInstance.packageReference!!.fullQualifiedPath else Reference.OBJECT_TYPE_REFERENCE.fullQualifiedPath, @@ -35,8 +35,11 @@ object Table { ) } - fun findTypeInstance(classReference: Reference): TypeInstance? = - cachedClasses[classReference]?.typeInstance + fun findTypeInstance(typeReference: Reference): TypeInstance? = + cachedClasses[typeReference]?.typeInstance + + fun findFile(typeReference: Reference): File? = + cachedClasses[typeReference] fun hasClass(classReference: Reference): Boolean = cachedClasses.containsKey(classReference) diff --git a/src/main/kotlin/org/casc/lang/table/TypeUtil.kt b/src/main/kotlin/org/casc/lang/table/TypeUtil.kt index 0056fab1..136a5605 100644 --- a/src/main/kotlin/org/casc/lang/table/TypeUtil.kt +++ b/src/main/kotlin/org/casc/lang/table/TypeUtil.kt @@ -40,8 +40,7 @@ object TypeUtil { name.substring(name.length - 2) == "[]" -> { val baseType = asType(Reference(name.substring(0 until name.length - 2)), preference) - if (baseType == null) null - else ArrayType(baseType) + baseType?.let(::ArrayType) } else -> null } diff --git a/src/main/kotlin/org/casc/lang/table/Variable.kt b/src/main/kotlin/org/casc/lang/table/Variable.kt index 1b87bcff..dfcd3988 100644 --- a/src/main/kotlin/org/casc/lang/table/Variable.kt +++ b/src/main/kotlin/org/casc/lang/table/Variable.kt @@ -3,8 +3,25 @@ package org.casc.lang.table /** * Refers to function's local variable, its type might be null since its value could be null. */ -data class Variable(val mutable: Boolean, val name: String, var type: Type?, val index: Int, val declaredScopeDepth: Int) { - override fun equals(other: Any?): Boolean = - if (other !is Variable) false - else other.name == name +data class Variable( + val mutable: Boolean, + val name: String, + var type: Type?, + val index: Int, + val declaredScopeDepth: Int +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Variable + + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + return name.hashCode() + } } diff --git a/src/test/kotlin/CompilationTest.kt b/src/test/kotlin/CompilationTest.kt index 1cf35d5b..968e70c3 100644 --- a/src/test/kotlin/CompilationTest.kt +++ b/src/test/kotlin/CompilationTest.kt @@ -21,7 +21,7 @@ class CompilationTest { System.setOut(printStream) val localPreference = LocalPreference(enableColor = false, noEmit = true) - fileMap?.get("casc")?.forEach { + fileMap["casc"]?.forEach { localPreference.sourceFile = it val compilation = Compilation(localPreference) compilation.compile() @@ -47,4 +47,43 @@ class CompilationTest { return tests } + + @TestFactory + fun testPreludeChecker(): List { + val tests = mutableListOf() + val outputStream = ByteArrayOutputStream() + val printStream = PrintStream(outputStream) + val fileMap = File(Compilation::class.java.classLoader.getResource("preludeCheck")!!.file) + .listFiles()!! + .toList() + + System.setOut(printStream) + val localPreference = LocalPreference(enableColor = false, noEmit = true) + + fileMap.filter(File::isDirectory).forEach { + localPreference.sourceFile = it + val compilation = Compilation(localPreference) + compilation.compile() + + System.out.flush() + + val output = outputStream.toString().trim() + + val outFile = fileMap.find { file -> file.name == "${it.nameWithoutExtension}.out" } + + tests += if (outFile == null) { + DynamicTest.dynamicTest(it.name) { + Assertions.assertEquals("", output) + } + } else { + DynamicTest.dynamicTest(it.name) { + Assertions.assertEquals(outFile.readText(Charsets.UTF_8).trim().replace("\r", ""), output) + } + } + + outputStream.reset() + } + + return tests + } } \ No newline at end of file diff --git a/src/test/resources/parse/AbstractTrait.casc b/src/test/resources/parse/AbstractTrait.casc new file mode 100644 index 00000000..2b61dcf4 --- /dev/null +++ b/src/test/resources/parse/AbstractTrait.casc @@ -0,0 +1 @@ +abstr trait AbstractTrait \ No newline at end of file diff --git a/src/test/resources/parse/AbstractTrait.out b/src/test/resources/parse/AbstractTrait.out new file mode 100644 index 00000000..97e4eed5 --- /dev/null +++ b/src/test/resources/parse/AbstractTrait.out @@ -0,0 +1,4 @@ +error: Cannot declare trait instance with `abstr` keyword, trait is implicitly abstract +--> ./AbstractTrait.casc:1:0 +1 | abstr trait AbstractTrait + ^^^^^= hint: Remove this `abstr` keyword \ No newline at end of file diff --git a/src/test/resources/parse/DuplicateMut.casc b/src/test/resources/parse/DuplicateMut.casc index 2fc0e29f..714aed5a 100644 --- a/src/test/resources/parse/DuplicateMut.casc +++ b/src/test/resources/parse/DuplicateMut.casc @@ -1,5 +1,5 @@ mut mut class DuplicateMut impl DuplicateMut { - mut fn a1() {} + mut mut fn a1(self) {} } \ No newline at end of file diff --git a/src/test/resources/parse/DuplicateMut.out b/src/test/resources/parse/DuplicateMut.out index 6f0b3c3f..6fe595e5 100644 --- a/src/test/resources/parse/DuplicateMut.out +++ b/src/test/resources/parse/DuplicateMut.out @@ -7,4 +7,16 @@ error: Duplicate `mut` keyword --> ./DuplicateMut.casc:1:4 1 | mut mut class DuplicateMut ^^^= hint: Duplicate here -2 | \ No newline at end of file +2 | +error: Duplicate `mut` keyword +--> ./DuplicateMut.casc:4:4 +3 | impl DuplicateMut { +4 | mut mut fn a1(self) {} + ^^^= hint: Encountered first one here +5 | } +error: Duplicate `mut` keyword +--> ./DuplicateMut.casc:4:8 +3 | impl DuplicateMut { +4 | mut mut fn a1(self) {} + ^^^= hint: Duplicate here +5 | } \ No newline at end of file diff --git a/src/test/resources/parse/DuplicateOvrd.out b/src/test/resources/parse/DuplicateOvrd.out index 0bb92d15..53296b5f 100644 --- a/src/test/resources/parse/DuplicateOvrd.out +++ b/src/test/resources/parse/DuplicateOvrd.out @@ -3,8 +3,10 @@ error: Duplicate `ovrd` keyword 3 | impl DuplicateOvrd { 4 | ovrd ovrd fn a1() {} ^^^^= hint: Encountered first one here +5 | } error: Duplicate `ovrd` keyword --> ./DuplicateOvrd.casc:4:9 3 | impl DuplicateOvrd { 4 | ovrd ovrd fn a1() {} - ^^^^= hint: Duplicate here \ No newline at end of file + ^^^^= hint: Duplicate here +5 | } \ No newline at end of file diff --git a/src/test/resources/parse/IncompleteImpl.casc b/src/test/resources/parse/IncompleteImpl.casc new file mode 100644 index 00000000..36bfe73e --- /dev/null +++ b/src/test/resources/parse/IncompleteImpl.casc @@ -0,0 +1,3 @@ +class IncompleteImpl + +impl IncompleteImpl { \ No newline at end of file diff --git a/src/test/resources/parse/IncompleteImpl.out b/src/test/resources/parse/IncompleteImpl.out new file mode 100644 index 00000000..f05ae1e1 --- /dev/null +++ b/src/test/resources/parse/IncompleteImpl.out @@ -0,0 +1,5 @@ +error: Expected token CloseBrace, but got nothing +--> ./IncompleteImpl.casc:3:20 +2 | +3 | impl IncompleteImpl { + ^= hint: Reached last token \ No newline at end of file diff --git a/src/test/resources/parse/InvalidConstructor.casc b/src/test/resources/parse/InvalidConstructor.casc index bef42b18..c46e254c 100644 --- a/src/test/resources/parse/InvalidConstructor.casc +++ b/src/test/resources/parse/InvalidConstructor.casc @@ -2,8 +2,4 @@ class InvalidConstructor impl InvalidConstructor { new() : pub() - - new(a: i32) - - new(b: i32) } \ No newline at end of file diff --git a/src/test/resources/parse/InvalidConstructor.out b/src/test/resources/parse/InvalidConstructor.out index f4db0378..6b183ea6 100644 --- a/src/test/resources/parse/InvalidConstructor.out +++ b/src/test/resources/parse/InvalidConstructor.out @@ -3,9 +3,4 @@ error: Unexpected token, expected `super` or `self` keyword 3 | impl InvalidConstructor { 4 | new() : pub() ^^^ -5 | -error: Constructor `new`(i32) has already declared in same context ---> ./InvalidConstructor.casc:8:4 -7 | -8 | new(b: i32) - ^^^= hint: Try modify parameters' type \ No newline at end of file +5 | } \ No newline at end of file diff --git a/src/test/resources/parse/InvalidModifier.casc b/src/test/resources/parse/InvalidModifier.casc index 980476ea..93fa576f 100644 --- a/src/test/resources/parse/InvalidModifier.casc +++ b/src/test/resources/parse/InvalidModifier.casc @@ -6,4 +6,6 @@ impl InvalidModifier { } mut ovrd new() {} + + mut fn a1() {} } \ No newline at end of file diff --git a/src/test/resources/parse/InvalidModifier.out b/src/test/resources/parse/InvalidModifier.out index 67aafc32..51ad19cc 100644 --- a/src/test/resources/parse/InvalidModifier.out +++ b/src/test/resources/parse/InvalidModifier.out @@ -32,13 +32,22 @@ error: Cannot declare `ovrd` keyword after `mut` keyword 7 | 8 | mut ovrd new() {} ^^^= hint: `mut` keyword here +9 | error: Cannot declare constructor with `ovrd` keyword --> ./InvalidModifier.casc:8:8 7 | 8 | mut ovrd new() {} ^^^^= hint: Remove this `ovrd` keyword +9 | error: Cannot declare constructor with `mut` keyword --> ./InvalidModifier.casc:8:4 7 | 8 | mut ovrd new() {} - ^^^= hint: Remove this `mut` keyword \ No newline at end of file + ^^^= hint: Remove this `mut` keyword +9 | +error: Companion function cannot be mutable +--> ./InvalidModifier.casc:10:4 +9 | +10 | mut fn a1() {} + ^^^= hint: Remove this `mut` keyword +11 | } \ No newline at end of file diff --git a/src/test/resources/parse/InvalidModifierSequence.casc b/src/test/resources/parse/InvalidModifierSequence.casc index ebca3bef..2442ea06 100644 --- a/src/test/resources/parse/InvalidModifierSequence.casc +++ b/src/test/resources/parse/InvalidModifierSequence.casc @@ -1,7 +1,7 @@ mut priv class InvalidModifierSequence impl InvalidModifierSequence { - mut ovrd fn a1() {} + mut ovrd fn a1(self) {} ovrd priv fn a2() {} } \ No newline at end of file diff --git a/src/test/resources/parse/InvalidModifierSequence.out b/src/test/resources/parse/InvalidModifierSequence.out index b8d55fde..ebc79bbc 100644 --- a/src/test/resources/parse/InvalidModifierSequence.out +++ b/src/test/resources/parse/InvalidModifierSequence.out @@ -6,11 +6,12 @@ error: Cannot declare access modifier after `mut` keyword error: Cannot declare `ovrd` keyword after `mut` keyword --> ./InvalidModifierSequence.casc:4:4 3 | impl InvalidModifierSequence { -4 | mut ovrd fn a1() {} +4 | mut ovrd fn a1(self) {} ^^^= hint: `mut` keyword here 5 | error: Cannot declare access modifier after `ovrd` keyword --> ./InvalidModifierSequence.casc:6:4 5 | 6 | ovrd priv fn a2() {} - ^^^^= hint: `ovrd` keyword here \ No newline at end of file + ^^^^= hint: `ovrd` keyword here +7 | } \ No newline at end of file diff --git a/src/test/resources/parse/InvalidSelf.out b/src/test/resources/parse/InvalidSelf.out index cd837470..0ca33f50 100644 --- a/src/test/resources/parse/InvalidSelf.out +++ b/src/test/resources/parse/InvalidSelf.out @@ -14,4 +14,5 @@ error: `self` can only be at the start of parameters --> ./InvalidSelf.casc:8:18 7 | 8 | fn a1(i: i32, self) {} - ^^^^= hint: move `self` to the start of parameters \ No newline at end of file + ^^^^= hint: move `self` to the start of parameters +9 | } \ No newline at end of file diff --git a/src/test/resources/parse/MutableCompanionFunction.casc b/src/test/resources/parse/MutableCompanionFunction.casc new file mode 100644 index 00000000..878466e2 --- /dev/null +++ b/src/test/resources/parse/MutableCompanionFunction.casc @@ -0,0 +1,5 @@ +class MutableCompanionFunction + +impl MutableCompanionFunction { + mut fn a1() {} +} \ No newline at end of file diff --git a/src/test/resources/parse/MutableCompanionFunction.out b/src/test/resources/parse/MutableCompanionFunction.out new file mode 100644 index 00000000..4dd32af1 --- /dev/null +++ b/src/test/resources/parse/MutableCompanionFunction.out @@ -0,0 +1,6 @@ +error: Companion function cannot be mutable +--> ./MutableCompanionFunction.casc:4:4 +3 | impl MutableCompanionFunction { +4 | mut fn a1() {} + ^^^= hint: Remove this `mut` keyword +5 | } \ No newline at end of file diff --git a/src/test/resources/parse/MutableTrait.casc b/src/test/resources/parse/MutableTrait.casc new file mode 100644 index 00000000..91138572 --- /dev/null +++ b/src/test/resources/parse/MutableTrait.casc @@ -0,0 +1 @@ +mut trait MutableTrait \ No newline at end of file diff --git a/src/test/resources/parse/MutableTrait.out b/src/test/resources/parse/MutableTrait.out new file mode 100644 index 00000000..dc3e796d --- /dev/null +++ b/src/test/resources/parse/MutableTrait.out @@ -0,0 +1,4 @@ +error: Cannot declare trait instance with `mut` keyword, trait is implicitly mutable +--> ./MutableTrait.casc:1:0 +1 | mut trait MutableTrait + ^^^= hint: Remove this `mut` keyword \ No newline at end of file diff --git a/src/test/resources/parse/NonCompanionTraitField.casc b/src/test/resources/parse/NonCompanionTraitField.casc new file mode 100644 index 00000000..fe8d5fa3 --- /dev/null +++ b/src/test/resources/parse/NonCompanionTraitField.casc @@ -0,0 +1,3 @@ +trait NonCompanionTraitField { + a: i32 +} \ No newline at end of file diff --git a/src/test/resources/parse/NonCompanionTraitField.out b/src/test/resources/parse/NonCompanionTraitField.out new file mode 100644 index 00000000..16b114be --- /dev/null +++ b/src/test/resources/parse/NonCompanionTraitField.out @@ -0,0 +1,6 @@ +error: Cannot declare non-companion field a in trait +--> ./NonCompanionTraitField.casc:2:4 +1 | trait NonCompanionTraitField { +2 | a: i32 + ^= hint: Declare this field in companion block +3 | } \ No newline at end of file diff --git a/src/test/resources/parse/RedundantModifier.out b/src/test/resources/parse/RedundantModifier.out index b63f1027..c05e3330 100644 --- a/src/test/resources/parse/RedundantModifier.out +++ b/src/test/resources/parse/RedundantModifier.out @@ -13,4 +13,5 @@ error: Redundant access modifier `pub`, `pub` is declared under the hood by comp --> ./RedundantModifier.casc:6:4 5 | 6 | pub fn a1() {} - ^^^= hint: Remove this access modifier `pub` \ No newline at end of file + ^^^= hint: Remove this access modifier `pub` +7 | } \ No newline at end of file diff --git a/src/test/resources/preludeCheck/circularInheritance.out b/src/test/resources/preludeCheck/circularInheritance.out new file mode 100644 index 00000000..bfcd05cd --- /dev/null +++ b/src/test/resources/preludeCheck/circularInheritance.out @@ -0,0 +1,10 @@ +error: Circular inheritance is forbidden +--> ClassA.casc:3:14 +2 | +3 | impl ClassA : ClassB + ^^^^^^= hint: Class ClassA inherits class ClassB but class ClassB also inherits class ClassA +error: Circular inheritance is forbidden +--> ClassB.casc:3:14 +2 | +3 | impl ClassB : ClassA + ^^^^^^= hint: Class ClassB inherits class ClassA but class ClassA also inherits class ClassB \ No newline at end of file diff --git a/src/test/resources/preludeCheck/circularInheritance/ClassA.casc b/src/test/resources/preludeCheck/circularInheritance/ClassA.casc new file mode 100644 index 00000000..ec0e9c46 --- /dev/null +++ b/src/test/resources/preludeCheck/circularInheritance/ClassA.casc @@ -0,0 +1,3 @@ +mut class ClassA + +impl ClassA : ClassB \ No newline at end of file diff --git a/src/test/resources/preludeCheck/circularInheritance/ClassB.casc b/src/test/resources/preludeCheck/circularInheritance/ClassB.casc new file mode 100644 index 00000000..db2b6aaf --- /dev/null +++ b/src/test/resources/preludeCheck/circularInheritance/ClassB.casc @@ -0,0 +1,3 @@ +mut class ClassB + +impl ClassB : ClassA \ No newline at end of file diff --git a/src/test/resources/preludeCheck/functionSignatureDuplicate.out b/src/test/resources/preludeCheck/functionSignatureDuplicate.out new file mode 100644 index 00000000..a84db95b --- /dev/null +++ b/src/test/resources/preludeCheck/functionSignatureDuplicate.out @@ -0,0 +1,18 @@ +error: Constructor `new`(i32, i32) has already declared in same context +--> Class.casc:6:4 +5 | +6 | new(x: i32, y: i32) {} + ^^^= hint: Try modify parameters' type +7 | +error: Function a(i32, i32) has already declared in same context +--> Class.casc:10:7 +9 | +10 | fn a(x: i32, y: i32) {} + ^= hint: Try rename this function or modify parameters' type +11 | +error: Function a(i32, i32) has already declared in same context +--> Class.casc:14:7 +13 | +14 | fn a(self, x: i32, y: i32) {} + ^= hint: Try rename this function or modify parameters' type +15 | } \ No newline at end of file diff --git a/src/test/resources/preludeCheck/functionSignatureDuplicate/Class.casc b/src/test/resources/preludeCheck/functionSignatureDuplicate/Class.casc new file mode 100644 index 00000000..4b2516a9 --- /dev/null +++ b/src/test/resources/preludeCheck/functionSignatureDuplicate/Class.casc @@ -0,0 +1,15 @@ +class Class + +impl Class { + new(a: i32, b: i32) {} + + new(x: i32, y: i32) {} + + fn a(a: i32, b: i32) {} + + fn a(x: i32, y: i32) {} + + fn a(self, a: i32, b: i32) {} + + fn a(self, x: i32, y: i32) {} +} \ No newline at end of file diff --git a/src/test/resources/validate/project/Animal.casc b/src/test/resources/validate/project/Animal.casc index 05bf0e7a..a35e3f8b 100644 --- a/src/test/resources/validate/project/Animal.casc +++ b/src/test/resources/validate/project/Animal.casc @@ -1,18 +1,6 @@ trait Animal { - comp { - b: i32 - } } impl Animal { - fn main(args: [str]) { - System.out.println("Hello World") - a1() - } - - fn a1() {} - - fn a2(self) - - fn a4(self) {} + fn roar(self) } \ No newline at end of file diff --git a/src/test/resources/validate/project/Atom.casc b/src/test/resources/validate/project/Atom.casc index 7b80dc6c..63c738bf 100644 --- a/src/test/resources/validate/project/Atom.casc +++ b/src/test/resources/validate/project/Atom.casc @@ -7,4 +7,8 @@ impl Atom { mut fn hello(self) { java::lang::System.out.println("Atomic Powah!") } +} + +impl Atom for Animal { + ovrd fn roar(self) {} } \ No newline at end of file diff --git a/src/test/resources/validate/project/Matter.casc b/src/test/resources/validate/project/Matter.casc new file mode 100644 index 00000000..3d2ddd92 --- /dev/null +++ b/src/test/resources/validate/project/Matter.casc @@ -0,0 +1 @@ +abstr class Matter \ No newline at end of file diff --git a/src/test/resources/validate/project/Rock.casc b/src/test/resources/validate/project/Rock.casc index a51ac9e2..ae4ceb01 100644 --- a/src/test/resources/validate/project/Rock.casc +++ b/src/test/resources/validate/project/Rock.casc @@ -21,7 +21,7 @@ impl Rock: Atom { } else { new Rock("") } - a.hello() + a.roar() } mut fn lol(self): i32 {