diff --git a/compiler/build.gradle b/compiler/build.gradle index 731b0350a..0686b924e 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -70,7 +70,8 @@ sourceSets { } test { java { - srcDirs = ["${project.projectDir}/test"] + srcDir "${project.projectDir}/test" + srcDir "${project(':compilerAst').projectDir}/test/helpers" } } } diff --git a/compiler/src/prog8/compiler/Compiler.kt b/compiler/src/prog8/compiler/Compiler.kt index bc34795d5..894774ad5 100644 --- a/compiler/src/prog8/compiler/Compiler.kt +++ b/compiler/src/prog8/compiler/Compiler.kt @@ -16,12 +16,11 @@ import prog8.compiler.target.Cx16Target import prog8.compiler.target.ICompilationTarget import prog8.compiler.target.asmGeneratorFor import prog8.optimizer.* -import prog8.parser.ModuleImporter import prog8.parser.ParsingFailedError -import prog8.parser.moduleName import java.io.File import java.io.InputStream import java.nio.file.Path +import kotlin.io.path.* import kotlin.system.measureTimeMillis @@ -135,7 +134,7 @@ fun compileProgram(filepath: Path, throw x } - val failedProgram = Program("failed", mutableListOf(), BuiltinFunctionsFacade(BuiltinFunctions), compTarget) + val failedProgram = Program("failed", BuiltinFunctionsFacade(BuiltinFunctions), compTarget) return CompilationResult(false, failedProgram, programName, compTarget, emptyList()) } @@ -169,27 +168,29 @@ private class BuiltinFunctionsFacade(functions: Map): IBuilt builtinFunctionReturnType(name, args, program) } -private fun parseImports(filepath: Path, +fun parseImports(filepath: Path, errors: IErrorReporter, compTarget: ICompilationTarget, libdirs: List): Triple> { - val compilationTargetName = compTarget.name - println("Compiler target: $compilationTargetName. Parsing...") + println("Compiler target: ${compTarget.name}. Parsing...") val bf = BuiltinFunctionsFacade(BuiltinFunctions) - val programAst = Program(moduleName(filepath.fileName), mutableListOf(), bf, compTarget) + val programAst = Program(filepath.nameWithoutExtension, bf, compTarget) bf.program = programAst - val importer = ModuleImporter(programAst, compTarget, compilationTargetName, libdirs) + val importer = ModuleImporter(programAst, compTarget.name, libdirs) importer.importModule(filepath) errors.report() - val importedFiles = programAst.modules.filter { !it.source.startsWith("@embedded@") }.map { it.source } + val importedFiles = programAst.modules + .mapNotNull { it.source } + .filter { !it.isFromResources } // TODO: parseImports/importedFiles - maybe rather `source.isFromFilesystem`? + .map { Path(it.pathString()) } val compilerOptions = determineCompilationOptions(programAst, compTarget) if (compilerOptions.launcher == LauncherType.BASIC && compilerOptions.output != OutputType.PRG) throw ParsingFailedError("${programAst.modules.first().position} BASIC launcher requires output type PRG.") // depending on the machine and compiler options we may have to include some libraries - for(lib in compTarget.machine.importLibs(compilerOptions, compilationTargetName)) + for(lib in compTarget.machine.importLibs(compilerOptions, compTarget.name)) importer.importLibraryModule(lib) // always import prog8_lib and math @@ -199,7 +200,7 @@ private fun parseImports(filepath: Path, return Triple(programAst, compilerOptions, importedFiles) } -private fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget): CompilationOptions { +fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget): CompilationOptions { val mainModule = program.mainModule val outputDirective = (mainModule.statements.singleOrNull { it is Directive && it.directive == "%output" } as? Directive) val launcherDirective = (mainModule.statements.singleOrNull { it is Directive && it.directive == "%launcher" } as? Directive) @@ -264,6 +265,9 @@ private fun processAst(programAst: Program, errors: IErrorReporter, compilerOpti println("Processing for target ${compilerOptions.compTarget.name}...") programAst.checkIdentifiers(errors, compilerOptions) errors.report() + // TODO: turning char literals into UBYTEs via an encoding should really happen in code gen - but for that we'd need DataType.CHAR + programAst.charLiteralsToUByteLiterals(errors, compilerOptions.compTarget) + errors.report() programAst.constantFold(errors, compilerOptions.compTarget) errors.report() programAst.reorderStatements(errors) @@ -346,14 +350,14 @@ fun printAst(programAst: Program) { println() } -fun loadAsmIncludeFile(filename: String, source: Path): String { - return if (filename.startsWith("library:")) { +fun loadAsmIncludeFile(filename: String, sourcePath: Path): String { + return if (filename.startsWith("library:")) { // FIXME: is the prefix "library:" or is it "@embedded@"? val resource = tryGetEmbeddedResource(filename.substring(8)) ?: throw IllegalArgumentException("library file '$filename' not found") resource.bufferedReader().use { it.readText() } } else { // first try in the isSameAs folder as where the containing file was imported from - val sib = source.resolveSibling(filename) + val sib = sourcePath.resolveSibling(filename) if (sib.toFile().isFile) sib.toFile().readText() else @@ -361,6 +365,9 @@ fun loadAsmIncludeFile(filename: String, source: Path): String { } } +/** + * Handle via SourceCode + */ internal fun tryGetEmbeddedResource(name: String): InputStream? { return object{}.javaClass.getResourceAsStream("/prog8lib/$name") } diff --git a/compiler/src/prog8/compiler/IStringEncoding.kt b/compiler/src/prog8/compiler/IStringEncoding.kt new file mode 100644 index 000000000..a69e9ba62 --- /dev/null +++ b/compiler/src/prog8/compiler/IStringEncoding.kt @@ -0,0 +1,6 @@ +package prog8.compiler + +interface IStringEncoding { + fun encodeString(str: String, altEncoding: Boolean): List + fun decodeString(bytes: List, altEncoding: Boolean): String +} \ No newline at end of file diff --git a/compiler/src/prog8/compiler/ModuleImporter.kt b/compiler/src/prog8/compiler/ModuleImporter.kt new file mode 100644 index 000000000..c18e32cc3 --- /dev/null +++ b/compiler/src/prog8/compiler/ModuleImporter.kt @@ -0,0 +1,140 @@ +package prog8.compiler + +import prog8.ast.Module +import prog8.ast.Program +import prog8.ast.base.Position +import prog8.ast.base.SyntaxError +import prog8.ast.statements.Directive +import prog8.ast.statements.DirectiveArg +import prog8.parser.Prog8Parser +import prog8.parser.SourceCode +import java.io.File +import java.nio.file.Path +import kotlin.io.FileSystemException +import kotlin.io.path.* + + +class ModuleImporter(private val program: Program, + private val compilationTargetName: String, + libdirs: List) { + + private val libpaths: List = libdirs.map { Path(it) } + + fun importModule(filePath: Path): Module { + val currentDir = Path("").absolute() + val searchIn = listOf(currentDir) + libpaths + val candidates = searchIn + .map { it.absolute().div(filePath).normalize().absolute() } + .filter { it.exists() } + .map { currentDir.relativize(it) } + .map { if (it.isAbsolute) it else Path(".", "$it") } + + val srcPath = when (candidates.size) { + 0 -> throw NoSuchFileException( + file = filePath.normalize().toFile(), + reason = "searched in $searchIn") + 1 -> candidates.first() + else -> candidates.first() // TODO: report error if more than 1 candidate? + } + + var logMsg = "importing '${filePath.nameWithoutExtension}' (from $srcPath)" + println(logMsg) + + return importModule(SourceCode.fromPath(srcPath)) + } + + fun importLibraryModule(name: String): Module? { + val import = Directive("%import", listOf( + DirectiveArg("", name, 42, position = Position("<<>>", 0, 0, 0)) + ), Position("<<>>", 0, 0, 0)) + return executeImportDirective(import, null) + } + + //private fun importModule(stream: CharStream, modulePath: Path, isLibrary: Boolean): Module { + private fun importModule(src: SourceCode) : Module { + val moduleAst = Prog8Parser.parseModule(src) + program.addModule(moduleAst) + + // accept additional imports + val lines = moduleAst.statements.toMutableList() + lines.asSequence() + .mapIndexed { i, it -> i to it } + .filter { (it.second as? Directive)?.directive == "%import" } + .forEach { executeImportDirective(it.second as Directive, moduleAst) } + + moduleAst.statements = lines + return moduleAst + } + + private fun executeImportDirective(import: Directive, importingModule: Module?): Module? { + if(import.directive!="%import" || import.args.size!=1 || import.args[0].name==null) + throw SyntaxError("invalid import directive", import.position) + val moduleName = import.args[0].name!! + if("$moduleName.p8" == import.position.file) + throw SyntaxError("cannot import self", import.position) + + val existing = program.modules.singleOrNull { it.name == moduleName } + if (existing!=null) + return null // TODO: why return null instead of Module instance? + + var srcCode = tryGetModuleFromResource("$moduleName.p8", compilationTargetName) + val importedModule = + if (srcCode != null) { + println("importing '$moduleName' (library): ${srcCode.origin}") + importModule(srcCode) + } else { + srcCode = tryGetModuleFromFile(moduleName, importingModule) + if (srcCode == null) + throw NoSuchFileException(File("$moduleName.p8")) + importModule(srcCode) + } + + removeDirectivesFromImportedModule(importedModule) + return importedModule + } + + private fun removeDirectivesFromImportedModule(importedModule: Module) { + // Most global directives don't apply for imported modules, so remove them + val moduleLevelDirectives = listOf("%output", "%launcher", "%zeropage", "%zpreserved", "%address", "%target") + var directives = importedModule.statements.filterIsInstance() + importedModule.statements.removeAll(directives) + directives = directives.filter{ it.directive !in moduleLevelDirectives } + importedModule.statements.addAll(0, directives) + } + + private fun tryGetModuleFromResource(name: String, compilationTargetName: String): SourceCode? { + // try target speficic first + try { + return SourceCode.fromResources("/prog8lib/$compilationTargetName/$name") + } catch (e: FileSystemException) { + } + try { + return SourceCode.fromResources("/prog8lib/$name") + } catch (e: FileSystemException) { + } + return null + } + + private fun tryGetModuleFromFile(name: String, importingModule: Module?): SourceCode? { + val fileName = "$name.p8" + val locations = + if (importingModule == null) { // <=> imported from library module + libpaths + } else { + libpaths.drop(1) + // TODO: why drop the first? + // FIXME: won't work until Prog8Parser is fixed s.t. it fully initialzes the modules it returns + listOf(Path(importingModule.position.file).parent ?: Path("")) + + listOf(Path(".", "prog8lib")) + } + + locations.forEach { + try { + return SourceCode.fromPath(it.resolve(fileName)) + } catch (e: NoSuchFileException) { + } + } + + //throw ParsingFailedError("$position Import: no module source file '$fileName' found (I've looked in: embedded libs and $locations)") + return null + } +} diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index fd5a88cc1..d5189df59 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -17,6 +17,7 @@ import prog8.compiler.target.Cx16Target import prog8.compiler.target.ICompilationTarget import java.io.CharConversionException import java.io.File +import kotlin.io.path.* import java.util.* internal class AstChecker(private val program: Program, @@ -721,11 +722,23 @@ internal class AstChecker(private val program: Program, } private fun checkFileExists(directive: Directive, filename: String) { - var definingModule = directive.parent + if (File(filename).isFile) + return + + var definingModule = directive.parent // TODO: why not just use directive.definingModule() here? while (definingModule !is Module) definingModule = definingModule.parent - if (!(filename.startsWith("library:") || definingModule.source.resolveSibling(filename).toFile().isFile || File(filename).isFile)) - errors.err("included file not found: $filename", directive.position) + if (definingModule.isLibrary()) + return + + val s = definingModule.source?.pathString() + if (s != null) { + val sourceFileCandidate = Path(s).resolveSibling(filename).toFile() + if (sourceFileCandidate.isFile) + return + } + + errors.err("included file not found: $filename", directive.position) } override fun visit(array: ArrayLiteralValue) { @@ -756,10 +769,20 @@ internal class AstChecker(private val program: Program, super.visit(array) } + override fun visit(char: CharLiteral) { + try { // just *try* if it can be encoded, don't actually do it + compTarget.encodeString(char.value.toString(), char.altEncoding) + } catch (cx: CharConversionException) { + errors.err(cx.message ?: "can't encode character", char.position) + } + + super.visit(char) + } + override fun visit(string: StringLiteralValue) { checkValueTypeAndRangeString(DataType.STR, string) - try { + try { // just *try* if it can be encoded, don't actually do it compTarget.encodeString(string.value, string.altEncoding) } catch (cx: CharConversionException) { errors.err(cx.message ?: "can't encode string", string.position) diff --git a/compiler/src/prog8/compiler/astprocessing/AstExtensions.kt b/compiler/src/prog8/compiler/astprocessing/AstExtensions.kt index 2497e5d69..92b7c1285 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstExtensions.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstExtensions.kt @@ -1,12 +1,65 @@ package prog8.compiler.astprocessing +import prog8.compiler.IStringEncoding +import prog8.ast.Node import prog8.ast.Program +import prog8.ast.base.DataType import prog8.ast.base.FatalAstException +import prog8.ast.expressions.* import prog8.ast.statements.Directive +import prog8.ast.walk.AstWalker +import prog8.ast.walk.IAstModification import prog8.compiler.BeforeAsmGenerationAstChanger import prog8.compiler.CompilationOptions import prog8.compiler.IErrorReporter import prog8.compiler.target.ICompilationTarget +import kotlin.math.abs + + +fun RangeExpr.size(encoding: IStringEncoding): Int? { + val fromLv = (from as? NumericLiteralValue) + val toLv = (to as? NumericLiteralValue) + if(fromLv==null || toLv==null) + return null + return toConstantIntegerRange(encoding)?.count() +} + +fun RangeExpr.toConstantIntegerRange(encoding: IStringEncoding): IntProgression? { + val fromVal: Int + val toVal: Int + val fromString = from as? StringLiteralValue + val toString = to as? StringLiteralValue + if(fromString!=null && toString!=null ) { + // string range -> int range over character values + fromVal = encoding.encodeString(fromString.value, fromString.altEncoding)[0].toInt() + toVal = encoding.encodeString(toString.value, fromString.altEncoding)[0].toInt() + } else { + val fromLv = from as? NumericLiteralValue + val toLv = to as? NumericLiteralValue + if(fromLv==null || toLv==null) + return null // non-constant range + // integer range + fromVal = fromLv.number.toInt() + toVal = toLv.number.toInt() + } + val stepVal = (step as? NumericLiteralValue)?.number?.toInt() ?: 1 + return makeRange(fromVal, toVal, stepVal) +} + +private fun makeRange(fromVal: Int, toVal: Int, stepVal: Int): IntProgression { + return when { + fromVal <= toVal -> when { + stepVal <= 0 -> IntRange.EMPTY + stepVal == 1 -> fromVal..toVal + else -> fromVal..toVal step stepVal + } + else -> when { + stepVal >= 0 -> IntRange.EMPTY + stepVal == -1 -> fromVal downTo toVal + else -> fromVal downTo toVal step abs(stepVal) + } + } +} internal fun Program.checkValid(compilerOptions: CompilationOptions, errors: IErrorReporter, compTarget: ICompilationTarget) { @@ -33,6 +86,20 @@ internal fun Program.reorderStatements(errors: IErrorReporter) { } } +internal fun Program.charLiteralsToUByteLiterals(errors: IErrorReporter, enc: IStringEncoding) { + val walker = object : AstWalker() { + override fun after(char: CharLiteral, parent: Node): Iterable { + return listOf(IAstModification.ReplaceNode( + char, + NumericLiteralValue(DataType.UBYTE, enc.encodeString(char.value.toString(), char.altEncoding)[0].toInt(), char.position), + parent + )) + } + } + walker.visit(this) + walker.applyModifications() +} + internal fun Program.addTypecasts(errors: IErrorReporter) { val caster = TypecastsAdder(this, errors) caster.visit(this) @@ -58,8 +125,24 @@ internal fun Program.checkIdentifiers(errors: IErrorReporter, options: Compilati lit2decl.applyModifications() } - if (modules.map { it.name }.toSet().size != modules.size) { - throw FatalAstException("modules should all be unique") + // Check if each module has a unique name. + // If not report those that haven't. + // TODO: move check for unique module names to earlier stage and/or to unit tests + val namesToModules = mapOf>().toMutableMap() + for (m in modules) { + var others = namesToModules[m.name] + if (others == null) { + namesToModules.put(m.name, listOf(m).toMutableList()) + } else { + others.add(m) + } + } + val nonUniqueNames = namesToModules.keys + .map { Pair(it, namesToModules[it]!!.size) } + .filter { it.second > 1 } + .map { "\"${it.first}\" (x${it.second})"} + if (nonUniqueNames.size > 0) { + throw FatalAstException("modules must have unique names; of the ttl ${modules.size} these have not: $nonUniqueNames") } } @@ -80,9 +163,7 @@ internal fun Program.moveMainAndStartToFirst() { val start = this.entrypoint() val mod = start.definingModule() val block = start.definingBlock() - if(!modules.remove(mod)) - throw FatalAstException("module wrong") - modules.add(0, mod) + moveModuleToFront(mod) mod.remove(block) var afterDirective = mod.statements.indexOfFirst { it !is Directive } if(afterDirective<0) diff --git a/compiler/src/prog8/compiler/target/ICompilationTarget.kt b/compiler/src/prog8/compiler/target/ICompilationTarget.kt index b778e3182..d232f89f9 100644 --- a/compiler/src/prog8/compiler/target/ICompilationTarget.kt +++ b/compiler/src/prog8/compiler/target/ICompilationTarget.kt @@ -1,7 +1,7 @@ package prog8.compiler.target import prog8.ast.IMemSizer -import prog8.ast.IStringEncoding +import prog8.compiler.IStringEncoding import prog8.ast.Program import prog8.ast.base.* import prog8.ast.expressions.IdentifierReference @@ -24,6 +24,8 @@ interface ICompilationTarget: IStringEncoding, IMemSizer { override fun encodeString(str: String, altEncoding: Boolean): List override fun decodeString(bytes: List, altEncoding: Boolean): String + // TODO: rename param target, and also AST node AssignTarget - *different meaning of "target"!* + // TODO: remove param program - can be obtained from AST node fun isInRegularRAM(target: AssignTarget, program: Program): Boolean { val memAddr = target.memoryAddress val arrayIdx = target.arrayindexed diff --git a/compiler/src/prog8/compiler/target/cpu6502/codegen/AsmGen.kt b/compiler/src/prog8/compiler/target/cpu6502/codegen/AsmGen.kt index 8ae826bdf..98ba68caa 100644 --- a/compiler/src/prog8/compiler/target/cpu6502/codegen/AsmGen.kt +++ b/compiler/src/prog8/compiler/target/cpu6502/codegen/AsmGen.kt @@ -14,10 +14,10 @@ import prog8.compiler.target.cpu6502.codegen.assignment.AsmAssignment import prog8.compiler.target.cpu6502.codegen.assignment.AssignmentAsmGen import prog8.optimizer.CallGraph import java.nio.file.Path -import java.nio.file.Paths import java.time.LocalDate import java.time.LocalDateTime import java.util.* +import kotlin.io.path.* import kotlin.math.absoluteValue @@ -1308,18 +1308,30 @@ $repeatLabel lda $counterVar } } + /** + * TODO: %asminclude and %asmbinary should be done earlier than code gen (-> put content into AST) + */ private fun translate(stmt: Directive) { when(stmt.directive) { "%asminclude" -> { - val sourcecode = loadAsmIncludeFile(stmt.args[0].str!!, stmt.definingModule().source) + // TODO: handle %asminclude with SourceCode + val includedName = stmt.args[0].str!! + val sourcePath = Path(stmt.definingModule().source!!.pathString()) // FIXME: %asminclude inside non-library, non-filesystem module + val sourcecode = loadAsmIncludeFile(includedName, sourcePath) assemblyLines.add(sourcecode.trimEnd().trimStart('\n')) } "%asmbinary" -> { + val includedName = stmt.args[0].str!! val offset = if(stmt.args.size>1) ", ${stmt.args[1].int}" else "" val length = if(stmt.args.size>2) ", ${stmt.args[2].int}" else "" - val includedSourcePath = stmt.definingModule().source.resolveSibling(stmt.args[0].str) - val relPath = Paths.get("").relativize(includedSourcePath) - out(" .binary \"$relPath\" $offset $length") + val sourcePath = Path(stmt.definingModule().source!!.pathString()) // FIXME: %asmbinary inside non-library, non-filesystem module + val includedPath = sourcePath.resolveSibling(includedName) + val pathForAssembler = outputDir // #54: 64tass needs the path *relative to the .asm file* + .absolute() // avoid IllegalArgumentExc due to non-absolute path .relativize(absolute path) + .relativize(includedPath) + .normalize() // avoid assembler warnings (-Wportable; only some, not all) + .toString().replace('\\', '/') + out(" .binary \"$pathForAssembler\" $offset $length") } "%breakpoint" -> { val label = "_prog8_breakpoint_${breakpointLabels.size+1}" diff --git a/compiler/src/prog8/compiler/target/cpu6502/codegen/ForLoopsAsmGen.kt b/compiler/src/prog8/compiler/target/cpu6502/codegen/ForLoopsAsmGen.kt index 24e5229ec..226c4e7b8 100644 --- a/compiler/src/prog8/compiler/target/cpu6502/codegen/ForLoopsAsmGen.kt +++ b/compiler/src/prog8/compiler/target/cpu6502/codegen/ForLoopsAsmGen.kt @@ -9,6 +9,7 @@ import prog8.ast.expressions.RangeExpr import prog8.ast.statements.ForLoop import prog8.ast.toHex import prog8.compiler.AssemblyError +import prog8.compiler.astprocessing.toConstantIntegerRange import kotlin.math.absoluteValue internal class ForLoopsAsmGen(private val program: Program, private val asmgen: AsmGen) { @@ -19,7 +20,7 @@ internal class ForLoopsAsmGen(private val program: Program, private val asmgen: throw AssemblyError("unknown dt") when(stmt.iterable) { is RangeExpr -> { - val range = (stmt.iterable as RangeExpr).toConstantIntegerRange() + val range = (stmt.iterable as RangeExpr).toConstantIntegerRange(asmgen.options.compTarget) if(range==null) { translateForOverNonconstRange(stmt, iterableDt.typeOrElse(DataType.UNDEFINED), stmt.iterable as RangeExpr) } else { diff --git a/compiler/src/prog8/optimizer/ConstantFoldingOptimizer.kt b/compiler/src/prog8/optimizer/ConstantFoldingOptimizer.kt index d0315a620..9b6fc280a 100644 --- a/compiler/src/prog8/optimizer/ConstantFoldingOptimizer.kt +++ b/compiler/src/prog8/optimizer/ConstantFoldingOptimizer.kt @@ -9,11 +9,10 @@ import prog8.ast.statements.ForLoop import prog8.ast.statements.VarDecl import prog8.ast.walk.AstWalker import prog8.ast.walk.IAstModification -import prog8.compiler.target.ICompilationTarget import kotlin.math.pow -internal class ConstantFoldingOptimizer(private val program: Program, private val compTarget: ICompilationTarget) : AstWalker() { +internal class ConstantFoldingOptimizer(private val program: Program) : AstWalker() { override fun before(memread: DirectMemoryRead, parent: Node): Iterable { // @( &thing ) --> thing @@ -223,7 +222,7 @@ internal class ConstantFoldingOptimizer(private val program: Program, private va range.step } - return RangeExpr(fromCast.valueOrZero(), toCast.valueOrZero(), newStep, compTarget, range.position) + return RangeExpr(fromCast.valueOrZero(), toCast.valueOrZero(), newStep, range.position) } // adjust the datatype of a range expression in for loops to the loop variable. diff --git a/compiler/src/prog8/optimizer/ConstantIdentifierReplacer.kt b/compiler/src/prog8/optimizer/ConstantIdentifierReplacer.kt index 17d34933d..21d474181 100644 --- a/compiler/src/prog8/optimizer/ConstantIdentifierReplacer.kt +++ b/compiler/src/prog8/optimizer/ConstantIdentifierReplacer.kt @@ -9,6 +9,8 @@ import prog8.ast.statements.* import prog8.ast.walk.AstWalker import prog8.ast.walk.IAstModification import prog8.compiler.IErrorReporter +import prog8.compiler.astprocessing.size +import prog8.compiler.astprocessing.toConstantIntegerRange import prog8.compiler.target.ICompilationTarget // Fix up the literal value's type to match that of the vardecl @@ -154,14 +156,13 @@ internal class ConstantIdentifierReplacer(private val program: Program, private } } DataType.ARRAY_UB, DataType.ARRAY_B, DataType.ARRAY_UW, DataType.ARRAY_W -> { - val numericLv = decl.value as? NumericLiteralValue val rangeExpr = decl.value as? RangeExpr if(rangeExpr!=null) { // convert the initializer range expression to an actual array val declArraySize = decl.arraysize?.constIndex() - if(declArraySize!=null && declArraySize!=rangeExpr.size()) + if(declArraySize!=null && declArraySize!=rangeExpr.size(compTarget)) errors.err("range expression size doesn't match declared array size", decl.value?.position!!) - val constRange = rangeExpr.toConstantIntegerRange() + val constRange = rangeExpr.toConstantIntegerRange(compTarget) if(constRange!=null) { val eltType = rangeExpr.inferType(program).typeOrElse(DataType.UBYTE) val newValue = if(eltType in ByteDatatypes) { @@ -176,6 +177,7 @@ internal class ConstantIdentifierReplacer(private val program: Program, private return listOf(IAstModification.ReplaceNode(decl.value!!, newValue, decl)) } } + val numericLv = decl.value as? NumericLiteralValue if(numericLv!=null && numericLv.type== DataType.FLOAT) errors.err("arraysize requires only integers here", numericLv.position) val size = decl.arraysize?.constIndex() ?: return noModifications @@ -208,15 +210,13 @@ internal class ConstantIdentifierReplacer(private val program: Program, private } } DataType.ARRAY_F -> { - val size = decl.arraysize?.constIndex() ?: return noModifications - val litval = decl.value as? NumericLiteralValue val rangeExpr = decl.value as? RangeExpr if(rangeExpr!=null) { // convert the initializer range expression to an actual array of floats val declArraySize = decl.arraysize?.constIndex() - if(declArraySize!=null && declArraySize!=rangeExpr.size()) - errors.err("range expression size doesn't match declared array size", decl.value?.position!!) - val constRange = rangeExpr.toConstantIntegerRange() + if(declArraySize!=null && declArraySize!=rangeExpr.size(compTarget)) + errors.err("range expression size (${rangeExpr.size(compTarget)}) doesn't match declared array size ($declArraySize)", decl.value?.position!!) + val constRange = rangeExpr.toConstantIntegerRange(compTarget) if(constRange!=null) { val newValue = ArrayLiteralValue(InferredTypes.InferredType.known(DataType.ARRAY_F), constRange.map { NumericLiteralValue(DataType.FLOAT, it.toDouble(), decl.value!!.position) }.toTypedArray(), @@ -224,15 +224,18 @@ internal class ConstantIdentifierReplacer(private val program: Program, private return listOf(IAstModification.ReplaceNode(decl.value!!, newValue, decl)) } } - if(rangeExpr==null && litval!=null) { + + val numericLv = decl.value as? NumericLiteralValue + val size = decl.arraysize?.constIndex() ?: return noModifications + if(rangeExpr==null && numericLv!=null) { // arraysize initializer is a single int, and we know the size. - val fillvalue = litval.number.toDouble() + val fillvalue = numericLv.number.toDouble() if (fillvalue < compTarget.machine.FLOAT_MAX_NEGATIVE || fillvalue > compTarget.machine.FLOAT_MAX_POSITIVE) - errors.err("float value overflow", litval.position) + errors.err("float value overflow", numericLv.position) else { // create the array itself, filled with the fillvalue. - val array = Array(size) {fillvalue}.map { NumericLiteralValue(DataType.FLOAT, it, litval.position) }.toTypedArray() - val refValue = ArrayLiteralValue(InferredTypes.InferredType.known(DataType.ARRAY_F), array, position = litval.position) + val array = Array(size) {fillvalue}.map { NumericLiteralValue(DataType.FLOAT, it, numericLv.position) }.toTypedArray() + val refValue = ArrayLiteralValue(InferredTypes.InferredType.known(DataType.ARRAY_F), array, position = numericLv.position) return listOf(IAstModification.ReplaceNode(decl.value!!, refValue, decl)) } } diff --git a/compiler/src/prog8/optimizer/Extensions.kt b/compiler/src/prog8/optimizer/Extensions.kt index 89bd8910f..b69be66a7 100644 --- a/compiler/src/prog8/optimizer/Extensions.kt +++ b/compiler/src/prog8/optimizer/Extensions.kt @@ -21,7 +21,7 @@ internal fun Program.constantFold(errors: IErrorReporter, compTarget: ICompilati if(errors.noErrors()) { valuetypefixer.applyModifications() - val optimizer = ConstantFoldingOptimizer(this, compTarget) + val optimizer = ConstantFoldingOptimizer(this) optimizer.visit(this) while (errors.noErrors() && optimizer.applyModifications() > 0) { optimizer.visit(this) diff --git a/compiler/src/prog8/optimizer/StatementOptimizer.kt b/compiler/src/prog8/optimizer/StatementOptimizer.kt index 41355daa8..1db2c0b3f 100644 --- a/compiler/src/prog8/optimizer/StatementOptimizer.kt +++ b/compiler/src/prog8/optimizer/StatementOptimizer.kt @@ -11,6 +11,7 @@ import prog8.ast.walk.AstWalker import prog8.ast.walk.IAstModification import prog8.ast.walk.IAstVisitor import prog8.compiler.IErrorReporter +import prog8.compiler.astprocessing.size import prog8.compiler.target.ICompilationTarget import kotlin.math.floor @@ -197,7 +198,7 @@ internal class StatementOptimizer(private val program: Program, val range = forLoop.iterable as? RangeExpr if(range!=null) { - if(range.size()==1) { + if (range.size(compTarget) == 1) { // for loop over a (constant) range of just a single value-- optimize the loop away // loopvar/reg = range value , follow by block val scope = AnonymousScope(mutableListOf(), forLoop.position) diff --git a/compiler/test/AsmgenTests.kt b/compiler/test/AsmgenTests.kt index 8deeae874..4bbd85479 100644 --- a/compiler/test/AsmgenTests.kt +++ b/compiler/test/AsmgenTests.kt @@ -1,11 +1,11 @@ package prog8tests +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import prog8.ast.IBuiltinFunctions -import prog8.ast.IMemSizer +import prog8tests.helpers.* + import prog8.ast.Module import prog8.ast.Program import prog8.ast.base.* @@ -17,18 +17,9 @@ import prog8.compiler.target.c64.C64MachineDefinition import prog8.compiler.target.cpu6502.codegen.AsmGen import java.nio.file.Path + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestAsmGen6502 { - private class DummyFunctions: IBuiltinFunctions { - override val names: Set = emptySet() - override val purefunctionNames: Set = emptySet() - override fun constValue(name: String, args: List, position: Position, memsizer: IMemSizer): NumericLiteralValue? = null - override fun returnType(name: String, args: MutableList) = InferredTypes.InferredType.unknown() - } - - private class DummyMemsizer: IMemSizer { - override fun memorySize(dt: DataType): Int = 0 - } private fun createTestProgram(): Program { /* @@ -74,10 +65,10 @@ locallabel: val varInBlock = VarDecl(VarDeclType.VAR, DataType.UWORD, ZeropageWish.DONTCARE, null, "var_outside", null, false, false, false, Position.DUMMY) val block = Block("main", null, mutableListOf(labelInBlock, varInBlock, subroutine), false, Position.DUMMY) - val module = Module("test", mutableListOf(block), Position.DUMMY, Path.of("")) - module.linkParents(ParentSentinel) - val program = Program("test", mutableListOf(module), DummyFunctions(), DummyMemsizer()) - module.program = program + val module = Module("test", mutableListOf(block), Position.DUMMY, null) + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + module.linkParents(ParentSentinel) // TODO: why not module.linkParents(program.namespace)?! return program } diff --git a/compiler/test/ModuleImporterTests.kt b/compiler/test/ModuleImporterTests.kt new file mode 100644 index 000000000..19cef532a --- /dev/null +++ b/compiler/test/ModuleImporterTests.kt @@ -0,0 +1,322 @@ +package prog8tests + +import prog8tests.helpers.* +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.containsString +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.io.path.* + +import prog8.ast.Program +import prog8.parser.ParseError + +import prog8.compiler.ModuleImporter +import kotlin.test.assertContains + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestModuleImporter { + private val count = listOf("1st", "2nd", "3rd", "4th", "5th") + + private lateinit var program: Program + @BeforeEach + fun beforeEach() { + program = Program("foo", DummyFunctions, DummyMemsizer) + } + + private fun makeImporter(vararg searchIn: String): ModuleImporter = makeImporter(searchIn.asList()) + + private fun makeImporter(searchIn: Iterable) = + ModuleImporter(program, "blah", searchIn.toList()) + + @Nested + inner class Constructor { + + @Test + @Disabled("TODO: invalid entries in search list") + fun testInvalidEntriesInSearchList() {} + + @Test + @Disabled("TODO: literal duplicates in search list") + fun testLiteralDuplicatesInSearchList() {} + + @Test + @Disabled("TODO: factual duplicates in search list") + fun testFactualDuplicatesInSearchList() {} + } + + @Nested + inner class ImportModule { + + @Nested + inner class WithInvalidPath { + @Test + fun testNonexisting() { + val dirRel = assumeDirectory(".", workingDir.relativize(fixturesDir)) + val importer = makeImporter(dirRel.invariantSeparatorsPathString) + val srcPathRel = assumeNotExists(dirRel, "i_do_not_exist") + val srcPathAbs = srcPathRel.absolute() + + assertThrows { importer.importModule(srcPathRel) } + .let { + assertThat( + ".file should be normalized", + "${it.file}", equalTo("${it.file.normalize()}") + ) + assertThat( + ".file should point to specified path", + it.file.absolutePath, equalTo("${srcPathAbs.normalize()}") + ) + } + assertThat(program.modules.size, equalTo(1)) + + assertThrows { importer.importModule(srcPathAbs) } + .let { + assertThat( + ".file should be normalized", + "${it.file}", equalTo("${it.file.normalize()}") + ) + assertThat( + ".file should point to specified path", + it.file.absolutePath, equalTo("${srcPathAbs.normalize()}") + ) + } + assertThat(program.modules.size, equalTo(1)) + } + + @Test + fun testDirectory() { + val srcPathRel = assumeDirectory(workingDir.relativize(fixturesDir)) + val srcPathAbs = srcPathRel.absolute() + val searchIn = Path(".", "$srcPathRel").invariantSeparatorsPathString + val importer = makeImporter(searchIn) + + assertThrows { importer.importModule(srcPathRel) } + .let { + assertThat( + ".file should be normalized", + "${it.file}", equalTo("${it.file.normalize()}") + ) + assertThat( + ".file should point to specified path", + it.file.absolutePath, equalTo("${srcPathAbs.normalize()}") + ) + } + assertThat(program.modules.size, equalTo(1)) + + assertThrows { importer.importModule(srcPathAbs) } + .let { + assertThat( + ".file should be normalized", + "${it.file}", equalTo("${it.file.normalize()}") + ) + assertThat( + ".file should point to specified path", + it.file.absolutePath, equalTo("${srcPathAbs.normalize()}") + ) + } + assertThat(program.modules.size, equalTo(1)) + } + } + + @Nested + inner class WithValidPath { + + @Test + fun testAbsolute() { + val searchIn = listOf( + Path(".").div(workingDir.relativize(fixturesDir)), // we do want a dot "." in front + ).map { it.invariantSeparatorsPathString } + val importer = makeImporter(searchIn) + val fileName = "simple_main.p8" + val path = assumeReadableFile(searchIn[0], fileName) + + val module = importer.importModule(path.absolute()) + assertThat(program.modules.size, equalTo(2)) + assertContains(program.modules, module) + assertThat(module.program, equalTo(program)) + } + + @Test + fun testRelativeToWorkingDir() { + val searchIn = listOf( + Path(".").div(workingDir.relativize(fixturesDir)), // we do want a dot "." in front + ).map { it.invariantSeparatorsPathString } + val importer = makeImporter(searchIn) + val fileName = "simple_main.p8" + val path = assumeReadableFile(searchIn[0], fileName) + assertThat("sanity check: path should NOT be absolute", path.isAbsolute, equalTo(false)) + + val module = importer.importModule(path) + assertThat(program.modules.size, equalTo(2)) + assertContains(program.modules, module) + assertThat(module.program, equalTo(program)) + } + + @Test + fun testRelativeTo1stDirInSearchList() { + val searchIn = Path(".") + .div(workingDir.relativize(fixturesDir)) + .invariantSeparatorsPathString + val importer = makeImporter(searchIn) + val fileName = "simple_main.p8" + val path = Path(".", fileName) + assumeReadableFile(searchIn, path) + + val module = importer.importModule(path) + assertThat(program.modules.size, equalTo(2)) + assertContains(program.modules, module) + assertThat(module.program, equalTo(program)) + } + + @Test + @Disabled("TODO: relative to 2nd in search list") + fun testRelativeTo2ndDirInSearchList() {} + + @Test + @Disabled("TODO: ambiguous - 2 or more really different candidates") + fun testAmbiguousCandidates() {} + + @Nested + inner class WithBadFile { + @Test + fun testWithSyntaxError() { + val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir)) + val importer = makeImporter(searchIn.invariantSeparatorsPathString) + val srcPath = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8") + + val act = { importer.importModule(srcPath) } + + repeat(2) { n -> + assertThrows(count[n] + " call") { act() }.let { + assertThat(it.position.file, equalTo(srcPath.absolutePathString())) + assertThat("line; should be 1-based", it.position.line, equalTo(2)) + assertThat("startCol; should be 0-based", it.position.startCol, equalTo(6)) + assertThat("endCol; should be 0-based", it.position.endCol, equalTo(6)) + } + assertThat(program.modules.size, equalTo(1)) + } + } + + @Test + fun testImportingFileWithSyntaxError_once() { + doTestImportingFileWithSyntaxError(1) + } + + @Test + @Disabled("TODO: module that imports faulty module should not be kept in Program.modules") + fun testImportingFileWithSyntaxError_twice() { + doTestImportingFileWithSyntaxError(2) + } + + private fun doTestImportingFileWithSyntaxError(repetitions: Int) { + val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir)) + val importer = makeImporter(searchIn.invariantSeparatorsPathString) + val importing = assumeReadableFile(fixturesDir, "import_file_with_syntax_error.p8") + val imported = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8") + + val act = { importer.importModule(importing) } + + repeat(repetitions) { n -> + assertThrows(count[n] + " call") { act() }.let { + assertThat(it.position.file, equalTo(imported.absolutePathString())) + assertThat("line; should be 1-based", it.position.line, equalTo(2)) + assertThat("startCol; should be 0-based", it.position.startCol, equalTo(6)) + assertThat("endCol; should be 0-based", it.position.endCol, equalTo(6)) + } +// TODO("assertThat(program.modules.size, equalTo(2))") + } + } + } + } + + } + + @Nested + inner class ImportLibraryModule { + @Nested + inner class WithInvalidName { + @Test + fun testWithNonExistingName() { + val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir)) + val importer = makeImporter(searchIn.invariantSeparatorsPathString) + val filenameNoExt = assumeNotExists(fixturesDir, "i_do_not_exist").name + val filenameWithExt = assumeNotExists(fixturesDir, "i_do_not_exist.p8").name + + repeat(2) { n -> + assertThrows(count[n] + " call / NO .p8 extension") + { importer.importLibraryModule(filenameNoExt) }.let { + assertThat(it.message!!, containsString(filenameWithExt)) + } + assertThat(program.modules.size, equalTo(1)) + + assertThrows(count[n] + " call / with .p8 extension") + { importer.importLibraryModule(filenameWithExt) }.let { + assertThat(it.message!!, containsString(filenameWithExt)) + } + assertThat(program.modules.size, equalTo(1)) + } + } + } + + @Nested + inner class WithValidName { + @Nested + inner class WithBadFile { + @Test + fun testWithSyntaxError() { + val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir)) + val importer = makeImporter(searchIn.invariantSeparatorsPathString) + val srcPath = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8") + + repeat(2) { n -> + assertThrows(count[n] + " call") + { importer.importLibraryModule(srcPath.nameWithoutExtension) }.let { + assertThat(it.position.file, equalTo(srcPath.absolutePathString())) + assertThat("line; should be 1-based", it.position.line, equalTo(2)) + assertThat("startCol; should be 0-based", it.position.startCol, equalTo(6)) + assertThat("endCol; should be 0-based", it.position.endCol, equalTo(6)) + } + assertThat(program.modules.size, equalTo(1)) + } + } + + + private fun doTestImportingFileWithSyntaxError(repetitions: Int) { + val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir)) + val importer = makeImporter(searchIn.invariantSeparatorsPathString) + val importing = assumeReadableFile(fixturesDir, "import_file_with_syntax_error.p8") + val imported = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8") + + val act = { importer.importLibraryModule(importing.nameWithoutExtension) } + + repeat(repetitions) { n -> + assertThrows(count[n] + " call") { act() }.let { + assertThat(it.position.file, equalTo(imported.normalize().absolutePathString())) + assertThat("line; should be 1-based", it.position.line, equalTo(2)) + assertThat("startCol; should be 0-based", it.position.startCol, equalTo(6)) + assertThat("endCol; should be 0-based", it.position.endCol, equalTo(6)) + } +// TODO("assertThat(program.modules.size, equalTo(1))") + } + } + + @Test + fun testImportingFileWithSyntaxError_once() { + doTestImportingFileWithSyntaxError(1) + } + + @Test + @Disabled("TODO: module that imports faulty module should not be kept in Program.modules") + fun testImportingFileWithSyntaxError_twice() { + doTestImportingFileWithSyntaxError(2) + } + } + } + } +} diff --git a/compiler/test/TestCompilerOnCharLit.kt b/compiler/test/TestCompilerOnCharLit.kt index 3c6fcb600..5f82311bc 100644 --- a/compiler/test/TestCompilerOnCharLit.kt +++ b/compiler/test/TestCompilerOnCharLit.kt @@ -1,20 +1,19 @@ package prog8tests -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeAll +import kotlin.test.* +import prog8tests.helpers.* + import prog8.ast.IFunctionCall import prog8.ast.base.DataType import prog8.ast.base.VarDeclType import prog8.ast.expressions.IdentifierReference import prog8.ast.expressions.NumericLiteralValue -import prog8.compiler.compileProgram +import prog8.ast.expressions.RangeExpr +import prog8.ast.statements.ForLoop import prog8.compiler.target.Cx16Target -import kotlin.io.path.Path -import kotlin.io.path.absolute -import kotlin.io.path.isDirectory -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertTrue /** @@ -24,31 +23,18 @@ import kotlin.test.assertTrue */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestCompilerOnCharLit { - val workingDir = Path("").absolute() // Note: Path(".") does NOT work..! - val fixturesDir = workingDir.resolve("test/fixtures") - val outputDir = workingDir.resolve("build/tmp/test") - - @Test - fun testDirectoriesSanityCheck() { - assertEquals("compiler", workingDir.fileName.toString()) - assertTrue(fixturesDir.isDirectory(), "sanity check; should be directory: $fixturesDir") - assertTrue(outputDir.isDirectory(), "sanity check; should be directory: $outputDir") - } @Test fun testCharLitAsRomsubArg() { - val filepath = fixturesDir.resolve("charLitAsRomsubArg.p8") - val compilationTarget = Cx16Target - val result = compileProgram( - filepath, - optimize = false, - writeAssembly = true, - slowCodegenWarnings = false, - compilationTarget.name, - libdirs = listOf(), - outputDir - ) - assertTrue(result.success, "compilation should succeed") + val platform = Cx16Target + val result = compileText(platform, false, """ + main { + romsub ${"$"}FFD2 = chrout(ubyte ch @ A) + sub start() { + chrout('\n') + } + } + """).assertSuccess() val program = result.programAst val startSub = program.entrypoint() @@ -58,23 +44,22 @@ class TestCompilerOnCharLit { "char literal should have been replaced by ubyte literal") val arg = funCall.args[0] as NumericLiteralValue assertEquals(DataType.UBYTE, arg.type) - assertEquals(compilationTarget.encodeString("\n", false)[0], arg.number) + assertEquals(platform.encodeString("\n", false)[0], arg.number.toShort()) // TODO: short/int/UBYTE - which should it be? } @Test fun testCharVarAsRomsubArg() { - val filepath = fixturesDir.resolve("charVarAsRomsubArg.p8") - val compilationTarget = Cx16Target - val result = compileProgram( - filepath, - optimize = false, - writeAssembly = true, - slowCodegenWarnings = false, - compilationTarget.name, - libdirs = listOf(), - outputDir - ) - assertTrue(result.success, "compilation should succeed") + val platform = Cx16Target + val result = compileText(platform, false, """ + main { + romsub ${"$"}FFD2 = chrout(ubyte ch @ A) + sub start() { + ubyte ch = '\n' + chrout(ch) + } + } + """).assertSuccess() + val program = result.programAst val startSub = program.entrypoint() val funCall = startSub.statements.filterIsInstance()[0] @@ -94,23 +79,22 @@ class TestCompilerOnCharLit { "char literal should have been replaced by ubyte literal") val initializerValue = decl.value as NumericLiteralValue assertEquals(DataType.UBYTE, initializerValue.type) - assertEquals(compilationTarget.encodeString("\n", false)[0], initializerValue.number) + assertEquals(platform.encodeString("\n", false)[0], initializerValue.number.toShort()) // TODO: short/int/UBYTE - which should it be? } @Test fun testCharConstAsRomsubArg() { - val filepath = fixturesDir.resolve("charConstAsRomsubArg.p8") - val compilationTarget = Cx16Target - val result = compileProgram( - filepath, - optimize = false, - writeAssembly = true, - slowCodegenWarnings = false, - compilationTarget.name, - libdirs = listOf(), - outputDir - ) - assertTrue(result.success, "compilation should succeed") + val platform = Cx16Target + val result = compileText(platform, false, """ + main { + romsub ${"$"}FFD2 = chrout(ubyte ch @ A) + sub start() { + const ubyte ch = '\n' + chrout(ch) + } + } + """).assertSuccess() + val program = result.programAst val startSub = program.entrypoint() val funCall = startSub.statements.filterIsInstance()[0] @@ -122,16 +106,17 @@ class TestCompilerOnCharLit { assertEquals(VarDeclType.CONST, decl.type) assertEquals(DataType.UBYTE, decl.datatype) assertEquals( - compilationTarget.encodeString("\n", false)[0], - (decl.value as NumericLiteralValue).number) + platform.encodeString("\n", false)[0], + (decl.value as NumericLiteralValue).number.toShort()) // TODO: short/int/UBYTE - which should it be? } is NumericLiteralValue -> { assertEquals( - compilationTarget.encodeString("\n", false)[0], - arg.number) + platform.encodeString("\n", false)[0], + arg.number.toShort()) // TODO: short/int/UBYTE - which should it be? } else -> assertIs(funCall.args[0]) // make test fail } - } + } + diff --git a/compiler/test/TestCompilerOnExamples.kt b/compiler/test/TestCompilerOnExamples.kt index 3c5737a59..2fe47b74e 100644 --- a/compiler/test/TestCompilerOnExamples.kt +++ b/compiler/test/TestCompilerOnExamples.kt @@ -1,15 +1,18 @@ package prog8tests -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import prog8tests.helpers.* +import kotlin.io.path.* + import prog8.compiler.compileProgram +import prog8.compiler.target.C64Target import prog8.compiler.target.Cx16Target import prog8.compiler.target.ICompilationTarget -import kotlin.io.path.Path -import kotlin.io.path.absolute -import kotlin.io.path.isDirectory -import kotlin.test.assertEquals -import kotlin.test.assertTrue /** @@ -17,62 +20,105 @@ import kotlin.test.assertTrue * They are not really unit tests, but rather tests of the whole process, * from source file loading all the way through to running 64tass. */ +//@Disabled("to save some time") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestCompilerOnExamples { - val workingDir = Path("").absolute() // Note: Path(".") does NOT work..! - val examplesDir = workingDir.resolve("../examples") - val outputDir = workingDir.resolve("build/tmp/test") - - @Test - fun testDirectoriesSanityCheck() { - assertEquals("compiler", workingDir.fileName.toString()) - assertTrue(examplesDir.isDirectory(), "sanity check; should be directory: $examplesDir") - assertTrue(outputDir.isDirectory(), "sanity check; should be directory: $outputDir") - } + private val examplesDir = assumeDirectory(workingDir, "../examples") // TODO: make assembly stage testable - in case of failure (eg of 64tass) it Process.exit s - fun testExample(nameWithoutExt: String, platform: ICompilationTarget, optimize: Boolean) { - val filepath = examplesDir.resolve("$nameWithoutExt.p8") - val result = compileProgram( - filepath, - optimize, - writeAssembly = true, - slowCodegenWarnings = false, - compilationTarget = platform.name, - libdirs = listOf(), - outputDir - ) - assertTrue(result.success, - "compilation should succeed; ${platform.name}, optimize=$optimize: \"$filepath\"") + private fun makeDynamicCompilerTest(name: String, platform: ICompilationTarget, optimize: Boolean) : DynamicTest { + val searchIn = mutableListOf(examplesDir) + if (platform == Cx16Target) { + searchIn.add(0, assumeDirectory(examplesDir, "cx16")) + } + val filepath = searchIn + .map { it.resolve("$name.p8") } + .map { it.normalize().absolute() } + .map { workingDir.relativize(it) } + .first { it.exists() } + val displayName = "${examplesDir.relativize(filepath.absolute())}: ${platform.name}, optimize=$optimize" + return dynamicTest(displayName) { + compileProgram( + filepath, + optimize, + writeAssembly = true, + slowCodegenWarnings = false, + compilationTarget = platform.name, + libdirs = listOf(), + outputDir + ).assertSuccess("; $displayName") + } } + @TestFactory +// @Disabled + fun bothCx16AndC64() = mapCombinations( + dim1 = listOf( + "animals", + "balls", + "cube3d", + "cube3d-float", + "cube3d-gfx", + "cxlogo", + "dirlist", + "fibonacci", + "line-circle-gfx", + "line-circle-txt", + "mandelbrot", + "mandelbrot-gfx", + "numbergame", + "primes", + "rasterbars", + "screencodes", + "sorting", + "swirl", + "swirl-float", + "tehtriz", + "textelite", + ), + dim2 = listOf(Cx16Target, C64Target), + dim3 = listOf(false, true), + combine3 = ::makeDynamicCompilerTest + ) - @Test - fun test_cxLogo_noopt() { - testExample("cxlogo", Cx16Target, false) - } - @Test - fun test_cxLogo_opt() { - testExample("cxlogo", Cx16Target, true) - } - - @Test - fun test_swirl_noopt() { - testExample("swirl", Cx16Target, false) - } - @Test - fun test_swirl_opt() { - testExample("swirl", Cx16Target, true) - } - - @Test - fun test_animals_noopt() { - testExample("animals", Cx16Target, false) - } - @Test - fun test_animals_opt() { - testExample("animals", Cx16Target, true) - } + @TestFactory +// @Disabled + fun onlyC64() = mapCombinations( + dim1 = listOf( + "balloonflight", + "bdmusic", + "bdmusic-irq", + "charset", + "cube3d-sprites", + "plasma", + "sprites", + "turtle-gfx", + "wizzine", + ), + dim2 = listOf(C64Target), + dim3 = listOf(false, true), + combine3 = ::makeDynamicCompilerTest + ) + @TestFactory +// @Disabled + fun onlyCx16() = mapCombinations( + dim1 = listOf( + "vtui/testvtui", + "amiga", + "bobs", + "cobramk3-gfx", + "colorbars", + "datetime", + "highresbitmap", + "kefrenbars", + "mandelbrot-gfx-colors", + "multipalette", + "testgfx2", + ), + dim2 = listOf(Cx16Target), + dim3 = listOf(false, true), + combine3 = ::makeDynamicCompilerTest + ) } diff --git a/compiler/test/TestCompilerOnImportsAndIncludes.kt b/compiler/test/TestCompilerOnImportsAndIncludes.kt new file mode 100644 index 000000000..f70c7eede --- /dev/null +++ b/compiler/test/TestCompilerOnImportsAndIncludes.kt @@ -0,0 +1,154 @@ +package prog8tests + +import prog8tests.helpers.* +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import kotlin.test.* +import kotlin.io.path.* + +import prog8.ast.expressions.AddressOf +import prog8.ast.expressions.IdentifierReference +import prog8.ast.expressions.StringLiteralValue +import prog8.ast.statements.FunctionCallStatement +import prog8.ast.statements.Label +import prog8.compiler.target.Cx16Target + + +/** + * ATTENTION: this is just kludge! + * They are not really unit tests, but rather tests of the whole process, + * from source file loading all the way through to running 64tass. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestCompilerOnImportsAndIncludes { + + @Nested + inner class Import { + + @Test + fun testImportFromSameFolder() { + val filepath = assumeReadableFile(fixturesDir, "importFromSameFolder.p8") + assumeReadableFile(fixturesDir, "foo_bar.p8") + + val platform = Cx16Target + val result = compileFile(platform, optimize = false, fixturesDir, filepath.name) + .assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val strLits = startSub.statements + .filterIsInstance() + .map { it.args[0] as IdentifierReference } + .map { it.targetVarDecl(program)!!.value as StringLiteralValue } + + assertEquals("main.bar", strLits[0].value) + assertEquals("foo.bar", strLits[1].value) + assertEquals("main", strLits[0].definingScope().name) + assertEquals("foo", strLits[1].definingScope().name) + } + + @Test + @Disabled("TODO: why would we not accept string literals as argument to %import?") + fun testImportFromSameFolder_strLit() { + val filepath = assumeReadableFile(fixturesDir,"importFromSameFolder_strLit.p8") + val imported = assumeReadableFile(fixturesDir, "foo_bar.p8") + + val platform = Cx16Target + val result = compileFile(platform, optimize = false, fixturesDir, filepath.name) + .assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val strLits = startSub.statements + .filterIsInstance() + .map { it.args[0] as IdentifierReference } + .map { it.targetVarDecl(program)!!.value as StringLiteralValue } + + assertEquals("main.bar", strLits[0].value) + assertEquals("foo.bar", strLits[1].value) + assertEquals("main", strLits[0].definingScope().name) + assertEquals("foo", strLits[1].definingScope().name) + } + } + + @Nested + inner class AsmInclude { + @Test + fun testAsmIncludeFromSameFolder() { + val filepath = assumeReadableFile(fixturesDir, "asmIncludeFromSameFolder.p8") + assumeReadableFile(fixturesDir, "foo_bar.asm") + + val platform = Cx16Target + val result = compileFile(platform, optimize = false, fixturesDir, filepath.name) + .assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val args = startSub.statements + .filterIsInstance() + .map { it.args[0] } + + val str0 = (args[0] as IdentifierReference).targetVarDecl(program)!!.value as StringLiteralValue + assertEquals("main.bar", str0.value) + assertEquals("main", str0.definingScope().name) + + val id1 = (args[1] as AddressOf).identifier + val lbl1 = id1.targetStatement(program) as Label + assertEquals("foo_bar", lbl1.name) + assertEquals("start", lbl1.definingScope().name) + } + } + + @Nested + inner class Asmbinary { + @Test + fun testAsmbinaryDirectiveWithNonExistingFile() { + val p8Path = assumeReadableFile(fixturesDir, "asmBinaryNonExisting.p8") + assumeNotExists(fixturesDir, "i_do_not_exist.bin") + + compileFile(Cx16Target, false, p8Path.parent, p8Path.name, outputDir) + .assertFailure() + } + + @Test + fun testAsmbinaryDirectiveWithNonReadableFile() { + val p8Path = assumeReadableFile(fixturesDir, "asmBinaryNonReadable.p8") + assumeDirectory(fixturesDir, "subFolder") + + compileFile(Cx16Target, false, p8Path.parent, p8Path.name, outputDir) + .assertFailure() + } + + @TestFactory + fun asmbinaryDirectiveWithExistingBinFile(): Iterable = + listOf( + Triple("same ", "asmBinaryFromSameFolder.p8", "do_nothing1.bin"), + Triple("sub", "asmBinaryFromSubFolder.p8", "subFolder/do_nothing2.bin"), + ).map { + val (where, p8Str, binStr) = it + dynamicTest("%asmbinary from ${where}folder") { + val p8Path = assumeReadableFile(fixturesDir, p8Str) + val binPath = assumeReadableFile(fixturesDir, binStr) + assertNotEquals( // the bug we're testing for (#54) was hidden if outputDir == workinDir + workingDir.normalize().toAbsolutePath(), + outputDir.normalize().toAbsolutePath(), + "sanity check: workingDir and outputDir should not be the same folder" + ) + + compileFile(Cx16Target, false, p8Path.parent, p8Path.name, outputDir) + .assertSuccess( + "argument to assembler directive .binary " + + "should be relative to the generated .asm file (in output dir), " + + "NOT relative to .p8 neither current working dir" + ) + } + } + + } + +} diff --git a/compiler/test/TestCompilerOnRanges.kt b/compiler/test/TestCompilerOnRanges.kt new file mode 100644 index 000000000..6bc3501b1 --- /dev/null +++ b/compiler/test/TestCompilerOnRanges.kt @@ -0,0 +1,263 @@ +package prog8tests + +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.DynamicTest.dynamicTest +import kotlin.test.* +import prog8tests.helpers.* + +import prog8.ast.base.DataType +import prog8.ast.expressions.* +import prog8.ast.statements.ForLoop +import prog8.ast.statements.Subroutine +import prog8.ast.statements.VarDecl +import prog8.compiler.astprocessing.size +import prog8.compiler.astprocessing.toConstantIntegerRange +import prog8.compiler.target.C64Target +import prog8.compiler.target.Cx16Target + + +/** + * ATTENTION: this is just kludge! + * They are not really unit tests, but rather tests of the whole process, + * from source file loading all the way through to running 64tass. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestCompilerOnRanges { + + @Test + fun testUByteArrayInitializerWithRange_char_to_char() { + val platform = Cx16Target + val result = compileText(platform, true, """ + main { + sub start() { + ubyte[] cs = @'a' to 'z' ; values are computed at compile time + cs[0] = 23 ; keep optimizer from removing it + } + } + """).assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val decl = startSub + .statements.filterIsInstance()[0] + val rhsValues = (decl.value as ArrayLiteralValue) + .value // Array + .map { (it as NumericLiteralValue).number.toInt() } + val expectedStart = platform.encodeString("a", true)[0].toInt() + val expectedEnd = platform.encodeString("z", false)[0].toInt() + val expectedStr = "$expectedStart .. $expectedEnd" + + val actualStr = "${rhsValues.first()} .. ${rhsValues.last()}" + assertEquals(expectedStr, actualStr,".first .. .last") + assertEquals(expectedEnd - expectedStart + 1, rhsValues.last() - rhsValues.first() + 1, "rangeExpr.size()") + } + + @Test + fun testFloatArrayInitializerWithRange_char_to_char() { + val platform = C64Target + val result = compileText(platform, optimize = false, """ + %option enable_floats + main { + sub start() { + float[] cs = 'a' to 'z' ; values are computed at compile time + cs[0] = 23 ; keep optimizer from removing it + } + } + """).assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val decl = startSub + .statements.filterIsInstance()[0] + val rhsValues = (decl.value as ArrayLiteralValue) + .value // Array + .map { (it as NumericLiteralValue).number.toInt() } + val expectedStart = platform.encodeString("a", false)[0].toInt() + val expectedEnd = platform.encodeString("z", false)[0].toInt() + val expectedStr = "$expectedStart .. $expectedEnd" + + val actualStr = "${rhsValues.first()} .. ${rhsValues.last()}" + assertEquals(expectedStr, actualStr,".first .. .last") + assertEquals(expectedEnd - expectedStart + 1, rhsValues.size, "rangeExpr.size()") + } + + fun Subroutine.decl(varName: String): VarDecl { + return statements.filterIsInstance() + .first { it.name == varName } + } + inline fun VarDecl.rhs() : T { + return value as T + } + inline fun ArrayLiteralValue.elements() : List { + return value.map { it as T } + } + + fun assertEndpoints(expFirst: N, expLast: N, actual: Iterable, msg: String = ".first .. .last") { + val expectedStr = "$expFirst .. $expLast" + val actualStr = "${actual.first()} .. ${actual.last()}" + assertEquals(expectedStr, actualStr,".first .. .last") + } + + + @TestFactory + fun floatArrayInitializerWithRange() = mapCombinations( + dim1 = listOf("", "42", "41"), // sizeInDecl + dim2 = listOf("%option enable_floats", ""), // optEnableFloats + dim3 = listOf(Cx16Target, C64Target), // platform + dim4 = listOf(false, true), // optimize + combine4 = { sizeInDecl, optEnableFloats, platform, optimize -> + val displayName = + "test failed for: " + + when (sizeInDecl) { + "" -> "no" + "42" -> "correct" + else -> "wrong" + } + " array size given" + + ", " + (if (optEnableFloats == "") "without" else "with") + " %option enable_floats" + + ", ${platform.name}, optimize: $optimize" + dynamicTest(displayName) { + val result = compileText(platform, optimize, """ + $optEnableFloats + main { + sub start() { + float[$sizeInDecl] cs = 1 to 42 ; values are computed at compile time + cs[0] = 23 ; keep optimizer from removing it + } + } + """) + if (optEnableFloats != "" && (sizeInDecl=="" || sizeInDecl=="42")) + result.assertSuccess() + else + result.assertFailure() + } + } + ) + + @Test + fun testForLoopWithRange_char_to_char() { + val platform = Cx16Target + val result = compileText(platform, optimize = true, """ + main { + sub start() { + ubyte i + for i in @'a' to 'f' { + i += i ; keep optimizer from removing it + } + } + } + """).assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val iterable = startSub + .statements.filterIsInstance() + .map { it.iterable }[0] + val rangeExpr = iterable as RangeExpr + + val expectedStart = platform.encodeString("a", true)[0].toInt() + val expectedEnd = platform.encodeString("f", false)[0].toInt() + val expectedStr = "$expectedStart .. $expectedEnd" + + val intProgression = rangeExpr.toConstantIntegerRange(platform) + val actualStr = "${intProgression?.first} .. ${intProgression?.last}" + assertEquals(expectedStr, actualStr,".first .. .last") + assertEquals(expectedEnd - expectedStart + 1, rangeExpr.size(platform), "rangeExpr.size()") + } + + @Test + fun testForLoopWithRange_bool_to_bool() { + val platform = Cx16Target + val result = compileText(platform, optimize = true, """ + main { + sub start() { + ubyte i + for i in false to true { + i += i ; keep optimizer from removing it + } + } + } + """).assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val rangeExpr = startSub + .statements.filterIsInstance() + .map { it.iterable } + .filterIsInstance()[0] + + assertEquals(2, rangeExpr.size(platform)) + val intProgression = rangeExpr.toConstantIntegerRange(platform) + assertEquals(0, intProgression?.first) + assertEquals(1, intProgression?.last) + } + + @Test + fun testForLoopWithRange_ubyte_to_ubyte() { + val platform = Cx16Target + val result = compileText(platform, optimize = true, """ + main { + sub start() { + ubyte i + for i in 1 to 9 { + i += i ; keep optimizer from removing it + } + } + } + """).assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val rangeExpr = startSub + .statements.filterIsInstance() + .map { it.iterable } + .filterIsInstance()[0] + + assertEquals(9, rangeExpr.size(platform)) + val intProgression = rangeExpr.toConstantIntegerRange(platform) + assertEquals(1, intProgression?.first) + assertEquals(9, intProgression?.last) + } + + @Test + fun testForLoopWithRange_str_downto_str() { + compileText(Cx16Target, true, """ + main { + sub start() { + ubyte i + for i in "start" downto "end" { + i += i ; keep optimizer from removing it + } + } + } + """).assertFailure() + //TODO("test exact compile error(s)") + } + + @Test + fun testForLoopWithIterable_str() { + val result = compileText(Cx16Target, false, """ + main { + sub start() { + ubyte i + for i in "something" { + i += i ; keep optimizer from removing it + } + } + } + """).assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val iterable = startSub + .statements.filterIsInstance() + .map { it.iterable } + .filterIsInstance()[0] + + assertEquals(DataType.STR, iterable.inferType(program).typeOrElse(DataType.UNDEFINED)) + } + +} + diff --git a/compiler/test/TestCompilerOptionLibdirs.kt b/compiler/test/TestCompilerOptionLibdirs.kt new file mode 100644 index 000000000..4e577a085 --- /dev/null +++ b/compiler/test/TestCompilerOptionLibdirs.kt @@ -0,0 +1,94 @@ +package prog8tests + +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.AfterAll +import prog8tests.helpers.* +import kotlin.io.path.* +import java.nio.file.Path + +import prog8.compiler.compileProgram +import prog8.compiler.target.* + +/** + * ATTENTION: this is just kludge! + * They are not really unit tests, but rather tests of the whole process, + * from source file loading all the way through to running 64tass. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestCompilerOptionLibdirs { + + private lateinit var tempFileInWorkingDir: Path + + @BeforeAll + fun setUp() { + tempFileInWorkingDir = createTempFile(directory = workingDir, prefix = "tmp_", suffix = ".p8") + .also { it.writeText(""" + main { + sub start() { + } + } + """)} + } + + @AfterAll + fun tearDown() { + tempFileInWorkingDir.deleteExisting() + } + + private fun compileFile(filePath: Path, libdirs: List) = + compileProgram( + filepath = filePath, + optimize = false, + writeAssembly = true, + slowCodegenWarnings = false, + compilationTarget = Cx16Target.name, + libdirs, + outputDir + ) + + @Test + fun testAbsoluteFilePathInWorkingDir() { + val filepath = assumeReadableFile(tempFileInWorkingDir.absolute()) + compileFile(filepath, listOf()) + .assertSuccess() + } + + @Test + fun testFilePathInWorkingDirRelativeToWorkingDir() { + val filepath = assumeReadableFile(workingDir.relativize(tempFileInWorkingDir.absolute())) + compileFile(filepath, listOf()) + .assertSuccess() + } + + @Test + fun testFilePathInWorkingDirRelativeTo1stInLibdirs() { + val filepath = assumeReadableFile(tempFileInWorkingDir) + compileFile(filepath.fileName, listOf(workingDir.toString())) + .assertSuccess() + } + + @Test + fun testAbsoluteFilePathOutsideWorkingDir() { + val filepath = assumeReadableFile(fixturesDir, "simple_main.p8") + compileFile(filepath.absolute(), listOf()) + .assertSuccess() + } + + @Test + fun testFilePathOutsideWorkingDirRelativeToWorkingDir() { + val filepath = workingDir.relativize(assumeReadableFile(fixturesDir, "simple_main.p8").absolute()) + compileFile(filepath, listOf()) + .assertSuccess() + } + + @Test + fun testFilePathOutsideWorkingDirRelativeTo1stInLibdirs() { + val filepath = assumeReadableFile(fixturesDir, "simple_main.p8") + val libdirs = listOf("$fixturesDir") + compileFile(filepath.fileName, libdirs) + .assertSuccess() + } + +} diff --git a/compiler/test/TestImportedModulesOrderAndOptions.kt b/compiler/test/TestImportedModulesOrderAndOptions.kt new file mode 100644 index 000000000..7cb3d061b --- /dev/null +++ b/compiler/test/TestImportedModulesOrderAndOptions.kt @@ -0,0 +1,97 @@ +package prog8tests + +import org.junit.jupiter.api.* +import prog8.ast.internedStringsModuleName +import prog8.compiler.* +import prog8tests.helpers.* + +import prog8.compiler.target.C64Target +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestImportedModulesOrderAndOptions { + + @Test + fun testImportedModuleOrderCorrect() { + val result = compileText(C64Target, false, """ +%import textio +%import floats + +main { + sub start() { + ; nothing + } +} +""").assertSuccess() + assertTrue(result.programAst.mainModule.name.startsWith("on_the_fly_test")) + + val moduleNames = result.programAst.modules.map { it.name } + assertTrue(moduleNames[0].startsWith("on_the_fly_test"), "main module must be first") + assertEquals(listOf( + "prog8_interned_strings", + "textio", + "syslib", + "conv", + "floats", + "math", + "prog8_lib" + ), moduleNames.drop(1), "module order in parse tree") + } + + @Test + fun testCompilationOptionsCorrectFromMain() { + val result = compileText(C64Target, false, """ +%import textio +%import floats +%zeropage dontuse +%option no_sysinit + +main { + sub start() { + ; nothing + } +} +""").assertSuccess() + assertTrue(result.programAst.mainModule.name.startsWith("on_the_fly_test")) + val options = determineCompilationOptions(result.programAst, C64Target) + assertTrue(options.floats) + assertEquals(ZeropageType.DONTUSE, options.zeropage) + assertTrue(options.noSysInit) + } + + @Test + fun testModuleOrderAndCompilationOptionsCorrectWithJustImports() { + val errors = ErrorReporter() + val sourceText = """ +%import textio +%import floats +%option no_sysinit +%zeropage dontuse + +main { + sub start() { + ; nothing + } +} +""" + val filenameBase = "on_the_fly_test_" + sourceText.hashCode().toUInt().toString(16) + val filepath = outputDir.resolve("$filenameBase.p8") + filepath.toFile().writeText(sourceText) + val (program, options, importedfiles) = parseImports(filepath, errors, C64Target, emptyList()) + + assertEquals(filenameBase, program.mainModule.name) + assertEquals(1, importedfiles.size, "all imports other than the test source must have been internal resources library files") + assertEquals(listOf( + internedStringsModuleName, + filenameBase, + "textio", "syslib", "conv", "floats", "math", "prog8_lib" + ), program.modules.map {it.name}, "module order in parse tree") + assertTrue(options.floats) + assertEquals(ZeropageType.DONTUSE, options.zeropage, "zeropage option must be correctly taken from main module, not from float module import logic") + assertTrue(options.noSysInit) + } + + +} diff --git a/compiler/test/TestMemory.kt b/compiler/test/TestMemory.kt index b9ca856d5..598c0bb5f 100644 --- a/compiler/test/TestMemory.kt +++ b/compiler/test/TestMemory.kt @@ -1,8 +1,10 @@ package prog8tests +import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.Test -import prog8.ast.IBuiltinFunctions -import prog8.ast.IMemSizer +import kotlin.test.* +import prog8tests.helpers.* + import prog8.ast.Module import prog8.ast.Program import prog8.ast.base.DataType @@ -12,29 +14,17 @@ import prog8.ast.base.VarDeclType import prog8.ast.expressions.* import prog8.ast.statements.* import prog8.compiler.target.C64Target -import java.nio.file.Path -import kotlin.test.assertFalse -import kotlin.test.assertTrue +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestMemory { - private class DummyFunctions: IBuiltinFunctions { - override val names: Set = emptySet() - override val purefunctionNames: Set = emptySet() - override fun constValue(name: String, args: List, position: Position, memsizer: IMemSizer): NumericLiteralValue? = null - override fun returnType(name: String, args: MutableList) = InferredTypes.InferredType.unknown() - } - - private class DummyMemsizer: IMemSizer { - override fun memorySize(dt: DataType): Int = 0 - } - + @Test fun testInValidRamC64_memory_addresses() { var memexpr = NumericLiteralValue.optimalInteger(0x0000, Position.DUMMY) var target = AssignTarget(null, null, DirectMemoryWrite(memexpr, Position.DUMMY), Position.DUMMY) - val program = Program("test", mutableListOf(), DummyFunctions(), DummyMemsizer()) + val program = Program("test", DummyFunctions, DummyMemsizer) assertTrue(C64Target.isInRegularRAM(target, program)) memexpr = NumericLiteralValue.optimalInteger(0x1000, Position.DUMMY) @@ -59,7 +49,7 @@ class TestMemory { var memexpr = NumericLiteralValue.optimalInteger(0xa000, Position.DUMMY) var target = AssignTarget(null, null, DirectMemoryWrite(memexpr, Position.DUMMY), Position.DUMMY) - val program = Program("test", mutableListOf(), DummyFunctions(), DummyMemsizer()) + val program = Program("test", DummyFunctions, DummyMemsizer) assertFalse(C64Target.isInRegularRAM(target, program)) memexpr = NumericLiteralValue.optimalInteger(0xafff, Position.DUMMY) @@ -78,7 +68,7 @@ class TestMemory { @Test fun testInValidRamC64_memory_identifiers() { var target = createTestProgramForMemoryRefViaVar(0x1000, VarDeclType.VAR) - val program = Program("test", mutableListOf(), DummyFunctions(), DummyMemsizer()) + val program = Program("test", DummyFunctions, DummyMemsizer) assertTrue(C64Target.isInRegularRAM(target, program)) target = createTestProgramForMemoryRefViaVar(0xd020, VarDeclType.VAR) @@ -98,7 +88,7 @@ class TestMemory { val target = AssignTarget(null, null, DirectMemoryWrite(memexpr, Position.DUMMY), Position.DUMMY) val assignment = Assignment(target, NumericLiteralValue.optimalInteger(0, Position.DUMMY), Position.DUMMY) val subroutine = Subroutine("test", emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), null, false, false, mutableListOf(decl, assignment), Position.DUMMY) - val module = Module("test", mutableListOf(subroutine), Position.DUMMY, Path.of("")) + val module = Module("test", mutableListOf(subroutine), Position.DUMMY, null) module.linkParents(ParentSentinel) return target } @@ -107,7 +97,7 @@ class TestMemory { fun testInValidRamC64_memory_expression() { val memexpr = PrefixExpression("+", NumericLiteralValue.optimalInteger(0x1000, Position.DUMMY), Position.DUMMY) val target = AssignTarget(null, null, DirectMemoryWrite(memexpr, Position.DUMMY), Position.DUMMY) - val program = Program("test", mutableListOf(), DummyFunctions(), DummyMemsizer()) + val program = Program("test", DummyFunctions, DummyMemsizer) assertFalse(C64Target.isInRegularRAM(target, program)) } @@ -117,9 +107,10 @@ class TestMemory { val target = AssignTarget(IdentifierReference(listOf("address"), Position.DUMMY), null, null, Position.DUMMY) val assignment = Assignment(target, NumericLiteralValue.optimalInteger(0, Position.DUMMY), Position.DUMMY) val subroutine = Subroutine("test", emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), null, false, false, mutableListOf(decl, assignment), Position.DUMMY) - val module = Module("test", mutableListOf(subroutine), Position.DUMMY, Path.of("")) - val program = Program("test", mutableListOf(module), DummyFunctions(), DummyMemsizer()) - module.linkParents(ParentSentinel) + val module = Module("test", mutableListOf(subroutine), Position.DUMMY, null) + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + module.linkParents(ParentSentinel) // TODO: why not module.linkParents(program) or .linkParents(program.namespace)? assertTrue(C64Target.isInRegularRAM(target, program)) } @@ -130,9 +121,10 @@ class TestMemory { val target = AssignTarget(IdentifierReference(listOf("address"), Position.DUMMY), null, null, Position.DUMMY) val assignment = Assignment(target, NumericLiteralValue.optimalInteger(0, Position.DUMMY), Position.DUMMY) val subroutine = Subroutine("test", emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), null, false, false, mutableListOf(decl, assignment), Position.DUMMY) - val module = Module("test", mutableListOf(subroutine), Position.DUMMY, Path.of("")) - val program = Program("test", mutableListOf(module), DummyFunctions(), DummyMemsizer()) - module.linkParents(ParentSentinel) + val module = Module("test", mutableListOf(subroutine), Position.DUMMY, null) + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + module.linkParents(ParentSentinel) // TODO: why not module.linkParents(program) or .linkParents(program.namespace)? assertTrue(C64Target.isInRegularRAM(target, program)) } @@ -143,9 +135,10 @@ class TestMemory { val target = AssignTarget(IdentifierReference(listOf("address"), Position.DUMMY), null, null, Position.DUMMY) val assignment = Assignment(target, NumericLiteralValue.optimalInteger(0, Position.DUMMY), Position.DUMMY) val subroutine = Subroutine("test", emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), null, false, false, mutableListOf(decl, assignment), Position.DUMMY) - val module = Module("test", mutableListOf(subroutine), Position.DUMMY, Path.of("")) - val program = Program("test", mutableListOf(module), DummyFunctions(), DummyMemsizer()) - module.linkParents(ParentSentinel) + val module = Module("test", mutableListOf(subroutine), Position.DUMMY, null) + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + module.linkParents(ParentSentinel) // TODO: why not module.linkParents(program) or .linkParents(program.namespace)? assertFalse(C64Target.isInRegularRAM(target, program)) } @@ -156,9 +149,10 @@ class TestMemory { val target = AssignTarget(null, arrayindexed, null, Position.DUMMY) val assignment = Assignment(target, NumericLiteralValue.optimalInteger(0, Position.DUMMY), Position.DUMMY) val subroutine = Subroutine("test", emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), null, false, false, mutableListOf(decl, assignment), Position.DUMMY) - val module = Module("test", mutableListOf(subroutine), Position.DUMMY, Path.of("")) - val program = Program("test", mutableListOf(module), DummyFunctions(), DummyMemsizer()) - module.linkParents(ParentSentinel) + val module = Module("test", mutableListOf(subroutine), Position.DUMMY, null) + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + module.linkParents(ParentSentinel) // TODO: why not module.linkParents(program) or .linkParents(program.namespace)? assertTrue(C64Target.isInRegularRAM(target, program)) } @@ -170,9 +164,10 @@ class TestMemory { val target = AssignTarget(null, arrayindexed, null, Position.DUMMY) val assignment = Assignment(target, NumericLiteralValue.optimalInteger(0, Position.DUMMY), Position.DUMMY) val subroutine = Subroutine("test", emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), null, false, false, mutableListOf(decl, assignment), Position.DUMMY) - val module = Module("test", mutableListOf(subroutine), Position.DUMMY, Path.of("")) - val program = Program("test", mutableListOf(module), DummyFunctions(), DummyMemsizer()) - module.linkParents(ParentSentinel) + val module = Module("test", mutableListOf(subroutine), Position.DUMMY, null) + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + module.linkParents(ParentSentinel) // TODO: why not module.linkParents(program) or .linkParents(program.namespace)? assertTrue(C64Target.isInRegularRAM(target, program)) } @@ -184,9 +179,10 @@ class TestMemory { val target = AssignTarget(null, arrayindexed, null, Position.DUMMY) val assignment = Assignment(target, NumericLiteralValue.optimalInteger(0, Position.DUMMY), Position.DUMMY) val subroutine = Subroutine("test", emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), null, false, false, mutableListOf(decl, assignment), Position.DUMMY) - val module = Module("test", mutableListOf(subroutine), Position.DUMMY, Path.of("")) - val program = Program("test", mutableListOf(module), DummyFunctions(), DummyMemsizer()) - module.linkParents(ParentSentinel) + val module = Module("test", mutableListOf(subroutine), Position.DUMMY, null) + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + module.linkParents(ParentSentinel) // TODO: why not module.linkParents(program) or .linkParents(program.namespace)? assertFalse(C64Target.isInRegularRAM(target, program)) } } diff --git a/compiler/test/TestNumbers.kt b/compiler/test/TestNumbers.kt index 1de09e330..a22d98e68 100644 --- a/compiler/test/TestNumbers.kt +++ b/compiler/test/TestNumbers.kt @@ -1,17 +1,17 @@ package prog8tests +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import kotlin.test.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.closeTo import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance + import prog8.ast.toHex import prog8.compiler.CompilerException import prog8.compiler.target.c64.C64MachineDefinition.FLOAT_MAX_NEGATIVE import prog8.compiler.target.c64.C64MachineDefinition.FLOAT_MAX_POSITIVE import prog8.compiler.target.c64.C64MachineDefinition.Mflpt5 -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestNumbers { diff --git a/compiler/test/TestNumericLiteralValue.kt b/compiler/test/TestNumericLiteralValue.kt index 00666de65..5492fc2cf 100644 --- a/compiler/test/TestNumericLiteralValue.kt +++ b/compiler/test/TestNumericLiteralValue.kt @@ -1,19 +1,15 @@ package prog8tests -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import kotlin.test.* + import prog8.ast.base.DataType import prog8.ast.base.Position import prog8.ast.expressions.ArrayLiteralValue import prog8.ast.expressions.InferredTypes import prog8.ast.expressions.NumericLiteralValue import prog8.ast.expressions.StringLiteralValue -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue - - @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestNumericLiteralValue { diff --git a/compiler/test/TestPetscii.kt b/compiler/test/TestPetscii.kt index c83657810..f658e93c0 100644 --- a/compiler/test/TestPetscii.kt +++ b/compiler/test/TestPetscii.kt @@ -1,15 +1,16 @@ package prog8tests +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import kotlin.test.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance + import prog8.ast.base.DataType import prog8.ast.base.Position import prog8.ast.expressions.NumericLiteralValue import prog8.ast.expressions.StringLiteralValue import prog8.compiler.target.cbm.Petscii -import kotlin.test.* @TestInstance(TestInstance.Lifecycle.PER_CLASS) diff --git a/compiler/test/ZeropageTests.kt b/compiler/test/ZeropageTests.kt index 8e2a3a6b7..01622d02a 100644 --- a/compiler/test/ZeropageTests.kt +++ b/compiler/test/ZeropageTests.kt @@ -1,17 +1,15 @@ package prog8tests -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import kotlin.test.* + import prog8.ast.base.DataType import prog8.compiler.* import prog8.compiler.target.C64Target import prog8.compiler.target.Cx16Target import prog8.compiler.target.c64.C64MachineDefinition.C64Zeropage import prog8.compiler.target.cx16.CX16MachineDefinition.CX16Zeropage -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) diff --git a/compiler/test/fixtures/asmBinaryFromSameFolder.p8 b/compiler/test/fixtures/asmBinaryFromSameFolder.p8 new file mode 100644 index 000000000..d48a8c6b0 --- /dev/null +++ b/compiler/test/fixtures/asmBinaryFromSameFolder.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "do_nothing1.bin", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmBinaryFromSubFolder.p8 b/compiler/test/fixtures/asmBinaryFromSubFolder.p8 new file mode 100644 index 000000000..5b48b7892 --- /dev/null +++ b/compiler/test/fixtures/asmBinaryFromSubFolder.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "subFolder/do_nothing2.bin", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmBinaryNonExisting.p8 b/compiler/test/fixtures/asmBinaryNonExisting.p8 new file mode 100644 index 000000000..2ff4e2f46 --- /dev/null +++ b/compiler/test/fixtures/asmBinaryNonExisting.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "i_do_not_exist.bin", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmBinaryNonReadable.p8 b/compiler/test/fixtures/asmBinaryNonReadable.p8 new file mode 100644 index 000000000..3b7f63bfc --- /dev/null +++ b/compiler/test/fixtures/asmBinaryNonReadable.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "subFolder", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmIncludeFromSameFolder.p8 b/compiler/test/fixtures/asmIncludeFromSameFolder.p8 new file mode 100644 index 000000000..df25f6e78 --- /dev/null +++ b/compiler/test/fixtures/asmIncludeFromSameFolder.p8 @@ -0,0 +1,13 @@ +%import textio +main { + str myBar = "main.bar" +;foo_bar: +; %asminclude "foo_bar.asm" ; FIXME: should be accessible from inside start() but give assembler error + sub start() { + txt.print(myBar) + txt.print(&foo_bar) + return +foo_bar: + %asminclude "foo_bar.asm" + } +} diff --git a/compiler/test/fixtures/charConstAsRomsubArg.p8 b/compiler/test/fixtures/charConstAsRomsubArg.p8 deleted file mode 100644 index 838149365..000000000 --- a/compiler/test/fixtures/charConstAsRomsubArg.p8 +++ /dev/null @@ -1,7 +0,0 @@ -main { - romsub $FFD2 = chrout(ubyte ch @ A) - sub start() { - const ubyte ch = '\n' - chrout(ch) - } -} diff --git a/compiler/test/fixtures/charLitAsRomsubArg.p8 b/compiler/test/fixtures/charLitAsRomsubArg.p8 deleted file mode 100644 index ebcfa426e..000000000 --- a/compiler/test/fixtures/charLitAsRomsubArg.p8 +++ /dev/null @@ -1,6 +0,0 @@ -main { - romsub $FFD2 = chrout(ubyte ch @ A) - sub start() { - chrout('\n') - } -} diff --git a/compiler/test/fixtures/charVarAsRomsubArg.p8 b/compiler/test/fixtures/charVarAsRomsubArg.p8 deleted file mode 100644 index e04462345..000000000 --- a/compiler/test/fixtures/charVarAsRomsubArg.p8 +++ /dev/null @@ -1,7 +0,0 @@ -main { - romsub $FFD2 = chrout(ubyte ch @ A) - sub start() { - ubyte ch = '\n' - chrout(ch) - } -} diff --git a/compiler/test/fixtures/do_nothing.asm b/compiler/test/fixtures/do_nothing.asm new file mode 100644 index 000000000..6bfed22b6 --- /dev/null +++ b/compiler/test/fixtures/do_nothing.asm @@ -0,0 +1,7 @@ +; this is the source code for do_nothing1.bin and subFolder/do_nothing2.bin +; command lines: +; 64tass --ascii --nostart do_nothing.asm --output do_nothing1.bin +; 64tass --ascii --nostart do_nothing.asm --output subFolder/do_nothing2.bin +*=0 + rts + diff --git a/compiler/test/fixtures/do_nothing1.bin b/compiler/test/fixtures/do_nothing1.bin new file mode 100644 index 000000000..64845fb76 --- /dev/null +++ b/compiler/test/fixtures/do_nothing1.bin @@ -0,0 +1 @@ +` \ No newline at end of file diff --git a/compiler/test/fixtures/file_with_syntax_error.p8 b/compiler/test/fixtures/file_with_syntax_error.p8 new file mode 100644 index 000000000..6b9930fe0 --- /dev/null +++ b/compiler/test/fixtures/file_with_syntax_error.p8 @@ -0,0 +1,2 @@ +; test expects the following 2nd (!) line: +bad { } ; -> missing EOL at '}' (ie: *after* the '{') diff --git a/compiler/test/fixtures/foo_bar.asm b/compiler/test/fixtures/foo_bar.asm new file mode 100644 index 000000000..d415b6923 --- /dev/null +++ b/compiler/test/fixtures/foo_bar.asm @@ -0,0 +1,2 @@ +bar .text "foo.bar",0 + diff --git a/compiler/test/fixtures/foo_bar.p8 b/compiler/test/fixtures/foo_bar.p8 new file mode 100644 index 000000000..e60fa8199 --- /dev/null +++ b/compiler/test/fixtures/foo_bar.p8 @@ -0,0 +1,3 @@ +foo { + str bar = "foo.bar" +} diff --git a/compiler/test/fixtures/importFromSameFolder.p8 b/compiler/test/fixtures/importFromSameFolder.p8 new file mode 100644 index 000000000..3f233b10d --- /dev/null +++ b/compiler/test/fixtures/importFromSameFolder.p8 @@ -0,0 +1,9 @@ +%import textio +%import foo_bar +main { + str myBar = "main.bar" + sub start() { + txt.print(myBar) + txt.print(foo.bar) + } +} diff --git a/compiler/test/fixtures/importFromSameFolder_strLit.p8 b/compiler/test/fixtures/importFromSameFolder_strLit.p8 new file mode 100644 index 000000000..0f75c437f --- /dev/null +++ b/compiler/test/fixtures/importFromSameFolder_strLit.p8 @@ -0,0 +1,9 @@ +%import textio +%import "foo_bar.p8" +main { + str myBar = "main.bar" + sub start() { + txt.print(myBar) + txt.print(foo.bar) + } +} diff --git a/compiler/test/fixtures/import_file_with_syntax_error.p8 b/compiler/test/fixtures/import_file_with_syntax_error.p8 new file mode 100644 index 000000000..a6de5368f --- /dev/null +++ b/compiler/test/fixtures/import_file_with_syntax_error.p8 @@ -0,0 +1 @@ +%import file_with_syntax_error diff --git a/compiler/test/fixtures/import_import_nonexisting.p8 b/compiler/test/fixtures/import_import_nonexisting.p8 new file mode 100644 index 000000000..c9c418fa3 --- /dev/null +++ b/compiler/test/fixtures/import_import_nonexisting.p8 @@ -0,0 +1 @@ +%import import_nonexisting diff --git a/compiler/test/fixtures/import_nonexisting.p8 b/compiler/test/fixtures/import_nonexisting.p8 new file mode 100644 index 000000000..3d028ba09 --- /dev/null +++ b/compiler/test/fixtures/import_nonexisting.p8 @@ -0,0 +1 @@ +%import i_do_not_exist diff --git a/compiler/test/fixtures/simple_main.p8 b/compiler/test/fixtures/simple_main.p8 new file mode 100644 index 000000000..afaa79f93 --- /dev/null +++ b/compiler/test/fixtures/simple_main.p8 @@ -0,0 +1,4 @@ +main { + sub start() { + } +} diff --git a/compiler/test/fixtures/subFolder/do_nothing2.bin b/compiler/test/fixtures/subFolder/do_nothing2.bin new file mode 100644 index 000000000..64845fb76 --- /dev/null +++ b/compiler/test/fixtures/subFolder/do_nothing2.bin @@ -0,0 +1 @@ +` \ No newline at end of file diff --git a/compiler/test/helpers/compileXyz.kt b/compiler/test/helpers/compileXyz.kt new file mode 100644 index 000000000..1862b7e01 --- /dev/null +++ b/compiler/test/helpers/compileXyz.kt @@ -0,0 +1,60 @@ +package prog8tests.helpers + +import kotlin.test.* +import kotlin.io.path.* +import java.nio.file.Path + +import prog8.compiler.CompilationResult +import prog8.compiler.compileProgram +import prog8.compiler.target.ICompilationTarget + + +internal fun CompilationResult.assertSuccess(description: String = ""): CompilationResult { + assertTrue(success, "expected successful compilation but failed $description") + return this +} + +internal fun CompilationResult.assertFailure(description: String = ""): CompilationResult { + assertFalse(success, "expected failure to compile but succeeded $description") + return this +} + +/** + * @see CompilationResult.assertSuccess + * @see CompilationResult.assertFailure + */ +internal fun compileFile( + platform: ICompilationTarget, + optimize: Boolean, + fileDir: Path, + fileName: String, + outputDir: Path = prog8tests.helpers.outputDir +) : CompilationResult { + val filepath = fileDir.resolve(fileName) + assumeReadableFile(filepath) + return compileProgram( + filepath, + optimize, + writeAssembly = true, + slowCodegenWarnings = false, + platform.name, + libdirs = listOf(), + outputDir + ) +} + +/** + * Takes a [sourceText] as a String, writes it to a temporary + * file and then runs the compiler on that. + * @see compileFile + */ +internal fun compileText( + platform: ICompilationTarget, + optimize: Boolean, + sourceText: String +) : CompilationResult { + val filePath = outputDir.resolve("on_the_fly_test_" + sourceText.hashCode().toUInt().toString(16) + ".p8") + // we don't assumeNotExists(filePath) - should be ok to just overwrite it + filePath.toFile().writeText(sourceText) + return compileFile(platform, optimize, filePath.parent, filePath.name) +} diff --git a/compilerAst/build.gradle b/compilerAst/build.gradle index fe4ce75e9..a6be79821 100644 --- a/compilerAst/build.gradle +++ b/compilerAst/build.gradle @@ -45,15 +45,15 @@ sourceSets { main { java { srcDirs = ["${project.projectDir}/src"] - } - resources { - srcDirs = ["${project.projectDir}/res"] - } + } } test { java { srcDirs = ["${project.projectDir}/test"] } + resources { + srcDirs = ["${project.projectDir}/res"] + } } } diff --git a/compilerAst/compilerAst.iml b/compilerAst/compilerAst.iml index 240433064..24edf66c1 100644 --- a/compilerAst/compilerAst.iml +++ b/compilerAst/compilerAst.iml @@ -3,6 +3,7 @@ + diff --git a/compilerAst/res/prog8lib/math.asm b/compilerAst/res/prog8lib/math.asm new file mode 100644 index 000000000..2638821fb --- /dev/null +++ b/compilerAst/res/prog8lib/math.asm @@ -0,0 +1,20 @@ +; just for tests - DISFUNCTIONAL! + + +math_store_reg .byte 0 ; temporary storage + + +multiply_bytes .proc + ; -- multiply 2 bytes A and Y, result as byte in A (signed or unsigned) + sta P8ZP_SCRATCH_B1 ; num1 + sty P8ZP_SCRATCH_REG ; num2 + lda #0 + beq _enterloop +_doAdd clc + adc P8ZP_SCRATCH_B1 +_loop asl P8ZP_SCRATCH_B1 +_enterloop lsr P8ZP_SCRATCH_REG + bcs _doAdd + bne _loop + rts + .pend diff --git a/compilerAst/res/prog8lib/math.p8 b/compilerAst/res/prog8lib/math.p8 new file mode 100644 index 000000000..234079159 --- /dev/null +++ b/compilerAst/res/prog8lib/math.p8 @@ -0,0 +1,7 @@ +; Internal Math library routines - always included by the compiler +; +; Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 + +math { + %asminclude "library:math.asm" +} diff --git a/compilerAst/src/prog8/ast/AstToSourceCode.kt b/compilerAst/src/prog8/ast/AstToSourceCode.kt index 71fb3d7a5..6896f85ad 100644 --- a/compilerAst/src/prog8/ast/AstToSourceCode.kt +++ b/compilerAst/src/prog8/ast/AstToSourceCode.kt @@ -9,6 +9,11 @@ import prog8.ast.statements.* import prog8.ast.walk.IAstVisitor +/** + * Produces Prog8 source text from a [Program] (AST node), + * passing it as a String to the specified receiver function. + * TODO: rename/refactor to make proper sense in the presence of class [prog8.SourceCode] + */ class AstToSourceCode(val output: (text: String) -> Unit, val program: Program): IAstVisitor { private var scopelevel = 0 @@ -18,9 +23,9 @@ class AstToSourceCode(val output: (text: String) -> Unit, val program: Program): private fun outputi(s: Any) = output(indent(s.toString())) override fun visit(program: Program) { - outputln("============= PROGRAM ${program.name} (FROM AST) ===============") + outputln("; ============ PROGRAM ${program.name} (FROM AST) ==============") super.visit(program) - outputln("============= END PROGRAM ${program.name} (FROM AST) ===========") + outputln("; =========== END PROGRAM ${program.name} (FROM AST) ===========") } override fun visit(module: Module) { @@ -261,7 +266,15 @@ class AstToSourceCode(val output: (text: String) -> Unit, val program: Program): output(numLiteral.number.toString()) } + override fun visit(char: CharLiteral) { + if (char.altEncoding) + output("@") + output("'${escape(char.value.toString())}'") + } + override fun visit(string: StringLiteralValue) { + if (string.altEncoding) + output("@") output("\"${escape(string.value)}\"") } diff --git a/compilerAst/src/prog8/ast/AstToplevel.kt b/compilerAst/src/prog8/ast/AstToplevel.kt index 216d0bcb5..6d20882a5 100644 --- a/compilerAst/src/prog8/ast/AstToplevel.kt +++ b/compilerAst/src/prog8/ast/AstToplevel.kt @@ -5,57 +5,13 @@ import prog8.ast.expressions.* import prog8.ast.statements.* import prog8.ast.walk.AstWalker import prog8.ast.walk.IAstVisitor -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.io.path.name -import kotlin.math.abs +import prog8.parser.SourceCode const val internedStringsModuleName = "prog8_interned_strings" -interface IStringEncoding { - fun encodeString(str: String, altEncoding: Boolean): List - fun decodeString(bytes: List, altEncoding: Boolean): String -} -interface Node { - val position: Position - var parent: Node // will be linked correctly later (late init) - fun linkParents(parent: Node) - - fun definingModule(): Module { - if(this is Module) - return this - return findParentNode(this)!! - } - - fun definingSubroutine(): Subroutine? = findParentNode(this) - - fun definingScope(): INameScope { - val scope = findParentNode(this) - if(scope!=null) { - return scope - } - if(this is Label && this.name.startsWith("builtin::")) { - return BuiltinFunctionScopePlaceholder - } - if(this is GlobalNamespace) - return this - throw FatalAstException("scope missing from $this") - } - - fun definingBlock(): Block { - if(this is Block) - return this - return findParentNode(this)!! - } - - fun containingStatement(): Statement { - if(this is Statement) - return this - return findParentNode(this)!! - } - - fun replaceChildNode(node: Node, replacement: Node) +interface IAssignable { + // just a tag for now } interface IFunctionCall { @@ -63,7 +19,6 @@ interface IFunctionCall { var args: MutableList } - interface INameScope { val name: String val position: Position @@ -229,60 +184,107 @@ interface INameScope { } } -interface IAssignable { - // just a tag for now + +interface Node { + val position: Position + var parent: Node // will be linked correctly later (late init) + fun linkParents(parent: Node) + + fun definingModule(): Module { + if(this is Module) + return this + return findParentNode(this)!! + } + + fun definingSubroutine(): Subroutine? = findParentNode(this) + + fun definingScope(): INameScope { + val scope = findParentNode(this) + if(scope!=null) { + return scope + } + if(this is Label && this.name.startsWith("builtin::")) { + return BuiltinFunctionScopePlaceholder + } + if(this is GlobalNamespace) + return this + throw FatalAstException("scope missing from $this") + } + + fun definingBlock(): Block { + if(this is Block) + return this + return findParentNode(this)!! + } + + fun containingStatement(): Statement { + if(this is Statement) + return this + return findParentNode(this)!! + } + + fun replaceChildNode(node: Node, replacement: Node) } -interface IMemSizer { - fun memorySize(dt: DataType): Int -} - -interface IBuiltinFunctions { - val names: Set - val purefunctionNames: Set - fun constValue(name: String, args: List, position: Position, memsizer: IMemSizer): NumericLiteralValue? - fun returnType(name: String, args: MutableList): InferredTypes.InferredType -} /*********** Everything starts from here, the Program; zero or more modules *************/ - - class Program(val name: String, - val modules: MutableList, val builtinFunctions: IBuiltinFunctions, val memsizer: IMemSizer): Node { - val namespace = GlobalNamespace(modules, builtinFunctions.names) + private val _modules = mutableListOf() - val mainModule: Module + val modules: List = _modules + val namespace: GlobalNamespace = GlobalNamespace(modules, builtinFunctions.names) + + init { + // insert a container module for all interned strings later + val internedStringsModule = Module(internedStringsModuleName, mutableListOf(), Position.DUMMY, null) + val block = Block(internedStringsModuleName, null, mutableListOf(), true, Position.DUMMY) + internedStringsModule.statements.add(block) + + _modules.add(0, internedStringsModule) + internedStringsModule.linkParents(namespace) // TODO: was .linkParents(this) - probably wrong?! + internedStringsModule.program = this + } + + fun addModule(module: Module): Program { + require(null == _modules.firstOrNull { it.name == module.name }) + { "module '${module.name}' already present" } + _modules.add(module) + module.linkParents(namespace) + module.program = this + return this + } + + fun moveModuleToFront(module: Module): Program { + require(_modules.contains(module)) + { "Not a module of this program: '${module.name}'"} + _modules.remove(module) + _modules.add(0, module) + return this + } + + fun allBlocks(): List = modules.flatMap { it.statements.filterIsInstance() } + + fun entrypoint(): Subroutine { + val mainBlocks = allBlocks().filter { it.name=="main" } + return when (mainBlocks.size) { + 0 -> throw FatalAstException("no 'main' block") + 1 -> mainBlocks[0].subScope("start") as Subroutine + else -> throw FatalAstException("more than one 'main' block") + } + } + + val mainModule: Module // TODO: rename Program.mainModule - it's NOT necessarily the one containing the main *block*! get() = modules.first { it.name!=internedStringsModuleName } + val definedLoadAddress: Int get() = mainModule.loadAddress var actualLoadAddress: Int = 0 private val internedStringsUnique = mutableMapOf, List>() - init { - // insert a container module for all interned strings later - if(modules.firstOrNull()?.name != internedStringsModuleName) { - val internedStringsModule = Module(internedStringsModuleName, mutableListOf(), Position.DUMMY, Path.of("")) - modules.add(0, internedStringsModule) - val block = Block(internedStringsModuleName, null, mutableListOf(), true, Position.DUMMY) - internedStringsModule.statements.add(block) - internedStringsModule.linkParents(this) - internedStringsModule.program = this - } - } - - fun entrypoint(): Subroutine { - val mainBlocks = allBlocks().filter { it.name=="main" } - if(mainBlocks.size > 1) - throw FatalAstException("more than one 'main' block") - if(mainBlocks.isEmpty()) - throw FatalAstException("no 'main' block") - return mainBlocks[0].subScope("start") as Subroutine - } - fun internString(string: StringLiteralValue): List { // Move a string literal into the internal, deduplicated, string pool // replace it with a variable declaration that points to the entry in the pool. @@ -316,10 +318,6 @@ class Program(val name: String, return scopedName } - - - fun allBlocks(): List = modules.flatMap { it.statements.filterIsInstance() } - override val position: Position = Position.DUMMY override var parent: Node get() = throw FatalAstException("program has no parent") @@ -333,16 +331,17 @@ class Program(val name: String, override fun replaceChildNode(node: Node, replacement: Node) { require(node is Module && replacement is Module) - val idx = modules.indexOfFirst { it===node } - modules[idx] = replacement - replacement.parent = this + val idx = _modules.indexOfFirst { it===node } + _modules[idx] = replacement + replacement.parent = this // TODO: why not replacement.program = this; replacement.linkParents(namespace)?! } + } -class Module(override val name: String, +open class Module(override val name: String, override var statements: MutableList, override val position: Position, - val source: Path) : Node, INameScope { + val source: SourceCode?) : Node, INameScope { override lateinit var parent: Node lateinit var program: Program @@ -370,19 +369,11 @@ class Module(override val name: String, fun accept(visitor: IAstVisitor) = visitor.visit(this) fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent) - companion object { - fun pathForResource(resourcePath: String): Path { - return Paths.get("@embedded@/$resourcePath") - } - - fun isLibrary(source: Path) = source.name=="" || source.startsWith("@embedded@/") - } - - fun isLibrary() = isLibrary(source) + fun isLibrary() = (source == null) || source.isFromResources } -class GlobalNamespace(val modules: List, private val builtinFunctionNames: Set): Node, INameScope { +class GlobalNamespace(val modules: Iterable, private val builtinFunctionNames: Set): Node, INameScope { override val name = "<<>>" override val position = Position("<<>>", 0, 0, 0) override val statements = mutableListOf() // not used @@ -429,18 +420,3 @@ object BuiltinFunctionScopePlaceholder : INameScope { } -fun Number.toHex(): String { - // 0..15 -> "0".."15" - // 16..255 -> "$10".."$ff" - // 256..65536 -> "$0100".."$ffff" - // negative values are prefixed with '-'. - val integer = this.toInt() - if(integer<0) - return '-' + abs(integer).toHex() - return when (integer) { - in 0 until 16 -> integer.toString() - in 0 until 0x100 -> "$"+integer.toString(16).padStart(2,'0') - in 0 until 0x10000 -> "$"+integer.toString(16).padStart(4,'0') - else -> throw IllegalArgumentException("number too large for 16 bits $this") - } -} diff --git a/compilerAst/src/prog8/ast/Extensions.kt b/compilerAst/src/prog8/ast/Extensions.kt new file mode 100644 index 000000000..5d6bf84eb --- /dev/null +++ b/compilerAst/src/prog8/ast/Extensions.kt @@ -0,0 +1,19 @@ +package prog8.ast + +import kotlin.math.abs + +fun Number.toHex(): String { + // 0..15 -> "0".."15" + // 16..255 -> "$10".."$ff" + // 256..65536 -> "$0100".."$ffff" + // negative values are prefixed with '-'. + val integer = this.toInt() + if(integer<0) + return '-' + abs(integer).toHex() + return when (integer) { + in 0 until 16 -> integer.toString() + in 0 until 0x100 -> "$"+integer.toString(16).padStart(2,'0') + in 0 until 0x10000 -> "$"+integer.toString(16).padStart(4,'0') + else -> throw IllegalArgumentException("number too large for 16 bits $this") + } +} \ No newline at end of file diff --git a/compilerAst/src/prog8/ast/IBuiltinFunctions.kt b/compilerAst/src/prog8/ast/IBuiltinFunctions.kt new file mode 100644 index 000000000..b9d19cf58 --- /dev/null +++ b/compilerAst/src/prog8/ast/IBuiltinFunctions.kt @@ -0,0 +1,13 @@ +package prog8.ast + +import prog8.ast.base.Position +import prog8.ast.expressions.Expression +import prog8.ast.expressions.InferredTypes +import prog8.ast.expressions.NumericLiteralValue + +interface IBuiltinFunctions { + val names: Set + val purefunctionNames: Set + fun constValue(name: String, args: List, position: Position, memsizer: IMemSizer): NumericLiteralValue? + fun returnType(name: String, args: MutableList): InferredTypes.InferredType +} \ No newline at end of file diff --git a/compilerAst/src/prog8/ast/IMemSizer.kt b/compilerAst/src/prog8/ast/IMemSizer.kt new file mode 100644 index 000000000..9ba6767e3 --- /dev/null +++ b/compilerAst/src/prog8/ast/IMemSizer.kt @@ -0,0 +1,7 @@ +package prog8.ast + +import prog8.ast.base.DataType + +interface IMemSizer { + fun memorySize(dt: DataType): Int +} \ No newline at end of file diff --git a/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt b/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt index 7240b95fe..f1e991129 100644 --- a/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt +++ b/compilerAst/src/prog8/ast/antlr/Antlr2Kotlin.kt @@ -1,32 +1,20 @@ package prog8.ast.antlr -import org.antlr.v4.runtime.IntStream import org.antlr.v4.runtime.ParserRuleContext import org.antlr.v4.runtime.tree.TerminalNode -import prog8.ast.IStringEncoding -import prog8.ast.Module import prog8.ast.base.* import prog8.ast.expressions.* import prog8.ast.statements.* -import prog8.parser.CustomLexer import prog8.parser.Prog8ANTLRParser -import java.io.CharConversionException -import java.io.File -import java.nio.file.Path /***************** Antlr Extension methods to create AST ****************/ private data class NumericLiteral(val number: Number, val datatype: DataType) -internal fun Prog8ANTLRParser.ModuleContext.toAst(name: String, source: Path, encoding: IStringEncoding) : Module { - val nameWithoutSuffix = if(name.endsWith(".p8")) name.substringBeforeLast('.') else name - val directives = this.directive().map { it.toAst() } - val blocks = this.block().map { it.toAst(Module.isLibrary(source), encoding) } - return Module(nameWithoutSuffix, (directives + blocks).toMutableList(), toPosition(), source) -} private fun ParserRuleContext.toPosition() : Position { + /* val customTokensource = this.start.tokenSource as? CustomLexer val filename = when { @@ -34,15 +22,18 @@ private fun ParserRuleContext.toPosition() : Position { start.tokenSource.sourceName == IntStream.UNKNOWN_SOURCE_NAME -> "@internal@" else -> File(start.inputStream.sourceName).name } + */ + val filename = start.inputStream.sourceName + // note: be ware of TAB characters in the source text, they count as 1 column... return Position(filename, start.line, start.charPositionInLine, stop.charPositionInLine + stop.text.length) } -private fun Prog8ANTLRParser.BlockContext.toAst(isInLibrary: Boolean, encoding: IStringEncoding) : Statement { +internal fun Prog8ANTLRParser.BlockContext.toAst(isInLibrary: Boolean) : Block { val blockstatements = block_statement().map { when { - it.variabledeclaration()!=null -> it.variabledeclaration().toAst(encoding) - it.subroutinedeclaration()!=null -> it.subroutinedeclaration().toAst(encoding) + it.variabledeclaration()!=null -> it.variabledeclaration().toAst() + it.subroutinedeclaration()!=null -> it.subroutinedeclaration().toAst() it.directive()!=null -> it.directive().toAst() it.inlineasm()!=null -> it.inlineasm().toAst() it.labeldef()!=null -> it.labeldef().toAst() @@ -52,11 +43,11 @@ private fun Prog8ANTLRParser.BlockContext.toAst(isInLibrary: Boolean, encoding: return Block(identifier().text, integerliteral()?.toAst()?.number?.toInt(), blockstatements.toMutableList(), isInLibrary, toPosition()) } -private fun Prog8ANTLRParser.Statement_blockContext.toAst(encoding: IStringEncoding): MutableList = - statement().asSequence().map { it.toAst(encoding) }.toMutableList() +private fun Prog8ANTLRParser.Statement_blockContext.toAst(): MutableList = + statement().asSequence().map { it.toAst() }.toMutableList() -private fun Prog8ANTLRParser.VariabledeclarationContext.toAst(encoding: IStringEncoding) : Statement { - vardecl()?.let { return it.toAst(encoding) } +private fun Prog8ANTLRParser.VariabledeclarationContext.toAst() : Statement { + vardecl()?.let { return it.toAst() } varinitializer()?.let { val vd = it.vardecl() @@ -64,9 +55,9 @@ private fun Prog8ANTLRParser.VariabledeclarationContext.toAst(encoding: IStringE VarDeclType.VAR, vd.datatype()?.toAst() ?: DataType.UNDEFINED, if (vd.ZEROPAGE() != null) ZeropageWish.PREFER_ZEROPAGE else ZeropageWish.DONTCARE, - vd.arrayindex()?.toAst(encoding), + vd.arrayindex()?.toAst(), vd.varname.text, - it.expression().toAst(encoding), + it.expression().toAst(), vd.ARRAYSIG() != null || vd.arrayindex() != null, false, vd.SHARED()!=null, @@ -81,9 +72,9 @@ private fun Prog8ANTLRParser.VariabledeclarationContext.toAst(encoding: IStringE VarDeclType.CONST, vd.datatype()?.toAst() ?: DataType.UNDEFINED, if (vd.ZEROPAGE() != null) ZeropageWish.PREFER_ZEROPAGE else ZeropageWish.DONTCARE, - vd.arrayindex()?.toAst(encoding), + vd.arrayindex()?.toAst(), vd.varname.text, - cvarinit.expression().toAst(encoding), + cvarinit.expression().toAst(), vd.ARRAYSIG() != null || vd.arrayindex() != null, false, vd.SHARED() != null, @@ -98,9 +89,9 @@ private fun Prog8ANTLRParser.VariabledeclarationContext.toAst(encoding: IStringE VarDeclType.MEMORY, vd.datatype()?.toAst() ?: DataType.UNDEFINED, if (vd.ZEROPAGE() != null) ZeropageWish.PREFER_ZEROPAGE else ZeropageWish.DONTCARE, - vd.arrayindex()?.toAst(encoding), + vd.arrayindex()?.toAst(), vd.varname.text, - mvarinit.expression().toAst(encoding), + mvarinit.expression().toAst(), vd.ARRAYSIG() != null || vd.arrayindex() != null, false, vd.SHARED()!=null, @@ -111,33 +102,33 @@ private fun Prog8ANTLRParser.VariabledeclarationContext.toAst(encoding: IStringE throw FatalAstException("weird variable decl $this") } -private fun Prog8ANTLRParser.SubroutinedeclarationContext.toAst(encoding: IStringEncoding) : Subroutine { +private fun Prog8ANTLRParser.SubroutinedeclarationContext.toAst() : Subroutine { return when { - subroutine()!=null -> subroutine().toAst(encoding) - asmsubroutine()!=null -> asmsubroutine().toAst(encoding) + subroutine()!=null -> subroutine().toAst() + asmsubroutine()!=null -> asmsubroutine().toAst() romsubroutine()!=null -> romsubroutine().toAst() else -> throw FatalAstException("weird subroutine decl $this") } } -private fun Prog8ANTLRParser.StatementContext.toAst(encoding: IStringEncoding) : Statement { - val vardecl = variabledeclaration()?.toAst(encoding) +private fun Prog8ANTLRParser.StatementContext.toAst() : Statement { + val vardecl = variabledeclaration()?.toAst() if(vardecl!=null) return vardecl assignment()?.let { - return Assignment(it.assign_target().toAst(encoding), it.expression().toAst(encoding), it.toPosition()) + return Assignment(it.assign_target().toAst(), it.expression().toAst(), it.toPosition()) } augassignment()?.let { // replace A += X with A = A + X - val target = it.assign_target().toAst(encoding) + val target = it.assign_target().toAst() val oper = it.operator.text.substringBefore('=') - val expression = BinaryExpression(target.toExpression(), oper, it.expression().toAst(encoding), it.expression().toPosition()) - return Assignment(it.assign_target().toAst(encoding), expression, it.toPosition()) + val expression = BinaryExpression(target.toExpression(), oper, it.expression().toAst(), it.expression().toPosition()) + return Assignment(it.assign_target().toAst(), expression, it.toPosition()) } postincrdecr()?.let { - return PostIncrDecr(it.assign_target().toAst(encoding), it.operator.text, it.toPosition()) + return PostIncrDecr(it.assign_target().toAst(), it.operator.text, it.toPosition()) } val directive = directive()?.toAst() @@ -149,49 +140,49 @@ private fun Prog8ANTLRParser.StatementContext.toAst(encoding: IStringEncoding) : val jump = unconditionaljump()?.toAst() if(jump!=null) return jump - val fcall = functioncall_stmt()?.toAst(encoding) + val fcall = functioncall_stmt()?.toAst() if(fcall!=null) return fcall - val ifstmt = if_stmt()?.toAst(encoding) + val ifstmt = if_stmt()?.toAst() if(ifstmt!=null) return ifstmt - val returnstmt = returnstmt()?.toAst(encoding) + val returnstmt = returnstmt()?.toAst() if(returnstmt!=null) return returnstmt - val subroutine = subroutinedeclaration()?.toAst(encoding) + val subroutine = subroutinedeclaration()?.toAst() if(subroutine!=null) return subroutine val asm = inlineasm()?.toAst() if(asm!=null) return asm - val branchstmt = branch_stmt()?.toAst(encoding) + val branchstmt = branch_stmt()?.toAst() if(branchstmt!=null) return branchstmt - val forloop = forloop()?.toAst(encoding) + val forloop = forloop()?.toAst() if(forloop!=null) return forloop - val untilloop = untilloop()?.toAst(encoding) + val untilloop = untilloop()?.toAst() if(untilloop!=null) return untilloop - val whileloop = whileloop()?.toAst(encoding) + val whileloop = whileloop()?.toAst() if(whileloop!=null) return whileloop - val repeatloop = repeatloop()?.toAst(encoding) + val repeatloop = repeatloop()?.toAst() if(repeatloop!=null) return repeatloop val breakstmt = breakstmt()?.toAst() if(breakstmt!=null) return breakstmt - val whenstmt = whenstmt()?.toAst(encoding) + val whenstmt = whenstmt()?.toAst() if(whenstmt!=null) return whenstmt throw FatalAstException("unprocessed source text (are we missing ast conversion rules for parser elements?): $text") } -private fun Prog8ANTLRParser.AsmsubroutineContext.toAst(encoding: IStringEncoding): Subroutine { +private fun Prog8ANTLRParser.AsmsubroutineContext.toAst(): Subroutine { val inline = this.inline()!=null val subdecl = asmsub_decl().toAst() - val statements = statement_block()?.toAst(encoding) ?: mutableListOf() + val statements = statement_block()?.toAst() ?: mutableListOf() return Subroutine(subdecl.name, subdecl.parameters, subdecl.returntypes, subdecl.asmParameterRegisters, subdecl.asmReturnvaluesRegisters, subdecl.asmClobbers, null, true, inline, statements, toPosition()) @@ -272,28 +263,28 @@ private fun Prog8ANTLRParser.Asmsub_paramsContext.toAst(): List AssignTarget(identifier.toAst(), null, null, toPosition()) - arrayindexed()!=null -> AssignTarget(null, arrayindexed().toAst(encoding), null, toPosition()) - directmemory()!=null -> AssignTarget(null, null, DirectMemoryWrite(directmemory().expression().toAst(encoding), toPosition()), toPosition()) + arrayindexed()!=null -> AssignTarget(null, arrayindexed().toAst(), null, toPosition()) + directmemory()!=null -> AssignTarget(null, null, DirectMemoryWrite(directmemory().expression().toAst(), toPosition()), toPosition()) else -> AssignTarget(scoped_identifier()?.toAst(), null, null, toPosition()) } } @@ -345,16 +336,16 @@ private fun Prog8ANTLRParser.ClobberContext.toAst() : Set { private fun Prog8ANTLRParser.DatatypeContext.toAst() = DataType.valueOf(text.uppercase()) -private fun Prog8ANTLRParser.ArrayindexContext.toAst(encoding: IStringEncoding) : ArrayIndex = - ArrayIndex(expression().toAst(encoding), toPosition()) +private fun Prog8ANTLRParser.ArrayindexContext.toAst() : ArrayIndex = + ArrayIndex(expression().toAst(), toPosition()) -private fun Prog8ANTLRParser.DirectiveContext.toAst() : Directive = +internal fun Prog8ANTLRParser.DirectiveContext.toAst() : Directive = Directive(directivename.text, directivearg().map { it.toAst() }, toPosition()) private fun Prog8ANTLRParser.DirectiveargContext.toAst() : DirectiveArg { val str = stringliteral() if(str?.ALT_STRING_ENCODING() != null) - throw AstException("${toPosition()} can't use alternate string encodings for directive arguments") + throw AstException("${toPosition()} can't use alternate string s for directive arguments") return DirectiveArg(stringliteral()?.text, identifier()?.text, integerliteral()?.toAst()?.number?.toInt(), toPosition()) } @@ -410,7 +401,7 @@ private fun Prog8ANTLRParser.IntegerliteralContext.toAst(): NumericLiteral { } } -private fun Prog8ANTLRParser.ExpressionContext.toAst(encoding: IStringEncoding) : Expression { +private fun Prog8ANTLRParser.ExpressionContext.toAst() : Expression { val litval = literalvalue() if(litval!=null) { @@ -431,17 +422,9 @@ private fun Prog8ANTLRParser.ExpressionContext.toAst(encoding: IStringEncoding) } litval.floatliteral()!=null -> NumericLiteralValue(DataType.FLOAT, litval.floatliteral().toAst(), litval.toPosition()) litval.stringliteral()!=null -> litval.stringliteral().toAst() - litval.charliteral()!=null -> { - try { - NumericLiteralValue(DataType.UBYTE, encoding.encodeString( - unescape(litval.charliteral().SINGLECHAR().text, litval.toPosition()), - litval.charliteral().ALT_STRING_ENCODING()!=null)[0], litval.toPosition()) - } catch (ce: CharConversionException) { - throw SyntaxError(ce.message ?: ce.toString(), litval.toPosition()) - } - } + litval.charliteral()!=null -> litval.charliteral().toAst() litval.arrayliteral()!=null -> { - val array = litval.arrayliteral().toAst(encoding) + val array = litval.arrayliteral().toAst() // the actual type of the arraysize can not yet be determined here (missing namespace & heap) // the ConstantFold takes care of that and converts the type if needed. ArrayLiteralValue(InferredTypes.InferredType.unknown(), array, position = litval.toPosition()) @@ -455,31 +438,31 @@ private fun Prog8ANTLRParser.ExpressionContext.toAst(encoding: IStringEncoding) return scoped_identifier().toAst() if(bop!=null) - return BinaryExpression(left.toAst(encoding), bop.text, right.toAst(encoding), toPosition()) + return BinaryExpression(left.toAst(), bop.text, right.toAst(), toPosition()) if(prefix!=null) - return PrefixExpression(prefix.text, expression(0).toAst(encoding), toPosition()) + return PrefixExpression(prefix.text, expression(0).toAst(), toPosition()) - val funcall = functioncall()?.toAst(encoding) + val funcall = functioncall()?.toAst() if(funcall!=null) return funcall if (rangefrom!=null && rangeto!=null) { val defaultstep = if(rto.text == "to") 1 else -1 - val step = rangestep?.toAst(encoding) ?: NumericLiteralValue(DataType.UBYTE, defaultstep, toPosition()) - return RangeExpr(rangefrom.toAst(encoding), rangeto.toAst(encoding), step, encoding, toPosition()) + val step = rangestep?.toAst() ?: NumericLiteralValue(DataType.UBYTE, defaultstep, toPosition()) + return RangeExpr(rangefrom.toAst(), rangeto.toAst(), step, toPosition()) } if(childCount==3 && children[0].text=="(" && children[2].text==")") - return expression(0).toAst(encoding) // expression within ( ) + return expression(0).toAst() // expression within ( ) if(arrayindexed()!=null) - return arrayindexed().toAst(encoding) + return arrayindexed().toAst() if(typecast()!=null) - return TypecastExpression(expression(0).toAst(encoding), typecast().datatype().toAst(), false, toPosition()) + return TypecastExpression(expression(0).toAst(), typecast().datatype().toAst(), false, toPosition()) if(directmemory()!=null) - return DirectMemoryRead(directmemory().expression().toAst(encoding), toPosition()) + return DirectMemoryRead(directmemory().expression().toAst(), toPosition()) if(addressof()!=null) return AddressOf(addressof().scoped_identifier().toAst(), toPosition()) @@ -487,16 +470,19 @@ private fun Prog8ANTLRParser.ExpressionContext.toAst(encoding: IStringEncoding) throw FatalAstException(text) } +private fun Prog8ANTLRParser.CharliteralContext.toAst(): CharLiteral = + CharLiteral(unescape(this.SINGLECHAR().text, toPosition())[0], this.ALT_STRING_ENCODING() != null, toPosition()) + private fun Prog8ANTLRParser.StringliteralContext.toAst(): StringLiteralValue = StringLiteralValue(unescape(this.STRING().text, toPosition()), ALT_STRING_ENCODING()!=null, toPosition()) -private fun Prog8ANTLRParser.ArrayindexedContext.toAst(encoding: IStringEncoding): ArrayIndexedExpression { +private fun Prog8ANTLRParser.ArrayindexedContext.toAst(): ArrayIndexedExpression { return ArrayIndexedExpression(scoped_identifier().toAst(), - arrayindex().toAst(encoding), + arrayindex().toAst(), toPosition()) } -private fun Prog8ANTLRParser.Expression_listContext.toAst(encoding: IStringEncoding) = expression().map{ it.toAst(encoding) } +private fun Prog8ANTLRParser.Expression_listContext.toAst() = expression().map{ it.toAst() } private fun Prog8ANTLRParser.IdentifierContext.toAst() : IdentifierReference = IdentifierReference(listOf(text), toPosition()) @@ -512,27 +498,27 @@ private fun Prog8ANTLRParser.BooleanliteralContext.toAst() = when(text) { else -> throw FatalAstException(text) } -private fun Prog8ANTLRParser.ArrayliteralContext.toAst(encoding: IStringEncoding) : Array = - expression().map { it.toAst(encoding) }.toTypedArray() +private fun Prog8ANTLRParser.ArrayliteralContext.toAst() : Array = + expression().map { it.toAst() }.toTypedArray() -private fun Prog8ANTLRParser.If_stmtContext.toAst(encoding: IStringEncoding): IfStatement { - val condition = expression().toAst(encoding) - val trueStatements = statement_block()?.toAst(encoding) ?: mutableListOf(statement().toAst(encoding)) - val elseStatements = else_part()?.toAst(encoding) ?: mutableListOf() +private fun Prog8ANTLRParser.If_stmtContext.toAst(): IfStatement { + val condition = expression().toAst() + val trueStatements = statement_block()?.toAst() ?: mutableListOf(statement().toAst()) + val elseStatements = else_part()?.toAst() ?: mutableListOf() val trueScope = AnonymousScope(trueStatements, statement_block()?.toPosition() ?: statement().toPosition()) val elseScope = AnonymousScope(elseStatements, else_part()?.toPosition() ?: toPosition()) return IfStatement(condition, trueScope, elseScope, toPosition()) } -private fun Prog8ANTLRParser.Else_partContext.toAst(encoding: IStringEncoding): MutableList { - return statement_block()?.toAst(encoding) ?: mutableListOf(statement().toAst(encoding)) +private fun Prog8ANTLRParser.Else_partContext.toAst(): MutableList { + return statement_block()?.toAst() ?: mutableListOf(statement().toAst()) } -private fun Prog8ANTLRParser.Branch_stmtContext.toAst(encoding: IStringEncoding): BranchStatement { +private fun Prog8ANTLRParser.Branch_stmtContext.toAst(): BranchStatement { val branchcondition = branchcondition().toAst() - val trueStatements = statement_block()?.toAst(encoding) ?: mutableListOf(statement().toAst(encoding)) - val elseStatements = else_part()?.toAst(encoding) ?: mutableListOf() + val trueStatements = statement_block()?.toAst() ?: mutableListOf(statement().toAst()) + val elseStatements = else_part()?.toAst() ?: mutableListOf() val trueScope = AnonymousScope(trueStatements, statement_block()?.toPosition() ?: statement().toPosition()) val elseScope = AnonymousScope(elseStatements, else_part()?.toPosition() ?: toPosition()) @@ -543,65 +529,65 @@ private fun Prog8ANTLRParser.BranchconditionContext.toAst() = BranchCondition.va text.substringAfter('_').uppercase() ) -private fun Prog8ANTLRParser.ForloopContext.toAst(encoding: IStringEncoding): ForLoop { +private fun Prog8ANTLRParser.ForloopContext.toAst(): ForLoop { val loopvar = identifier().toAst() - val iterable = expression()!!.toAst(encoding) + val iterable = expression()!!.toAst() val scope = if(statement()!=null) - AnonymousScope(mutableListOf(statement().toAst(encoding)), statement().toPosition()) + AnonymousScope(mutableListOf(statement().toAst()), statement().toPosition()) else - AnonymousScope(statement_block().toAst(encoding), statement_block().toPosition()) + AnonymousScope(statement_block().toAst(), statement_block().toPosition()) return ForLoop(loopvar, iterable, scope, toPosition()) } private fun Prog8ANTLRParser.BreakstmtContext.toAst() = Break(toPosition()) -private fun Prog8ANTLRParser.WhileloopContext.toAst(encoding: IStringEncoding): WhileLoop { - val condition = expression().toAst(encoding) - val statements = statement_block()?.toAst(encoding) ?: mutableListOf(statement().toAst(encoding)) +private fun Prog8ANTLRParser.WhileloopContext.toAst(): WhileLoop { + val condition = expression().toAst() + val statements = statement_block()?.toAst() ?: mutableListOf(statement().toAst()) val scope = AnonymousScope(statements, statement_block()?.toPosition() ?: statement().toPosition()) return WhileLoop(condition, scope, toPosition()) } -private fun Prog8ANTLRParser.RepeatloopContext.toAst(encoding: IStringEncoding): RepeatLoop { - val iterations = expression()?.toAst(encoding) - val statements = statement_block()?.toAst(encoding) ?: mutableListOf(statement().toAst(encoding)) +private fun Prog8ANTLRParser.RepeatloopContext.toAst(): RepeatLoop { + val iterations = expression()?.toAst() + val statements = statement_block()?.toAst() ?: mutableListOf(statement().toAst()) val scope = AnonymousScope(statements, statement_block()?.toPosition() ?: statement().toPosition()) return RepeatLoop(iterations, scope, toPosition()) } -private fun Prog8ANTLRParser.UntilloopContext.toAst(encoding: IStringEncoding): UntilLoop { - val untilCondition = expression().toAst(encoding) - val statements = statement_block()?.toAst(encoding) ?: mutableListOf(statement().toAst(encoding)) +private fun Prog8ANTLRParser.UntilloopContext.toAst(): UntilLoop { + val untilCondition = expression().toAst() + val statements = statement_block()?.toAst() ?: mutableListOf(statement().toAst()) val scope = AnonymousScope(statements, statement_block()?.toPosition() ?: statement().toPosition()) return UntilLoop(scope, untilCondition, toPosition()) } -private fun Prog8ANTLRParser.WhenstmtContext.toAst(encoding: IStringEncoding): WhenStatement { - val condition = expression().toAst(encoding) - val choices = this.when_choice()?.map { it.toAst(encoding) }?.toMutableList() ?: mutableListOf() +private fun Prog8ANTLRParser.WhenstmtContext.toAst(): WhenStatement { + val condition = expression().toAst() + val choices = this.when_choice()?.map { it.toAst() }?.toMutableList() ?: mutableListOf() return WhenStatement(condition, choices, toPosition()) } -private fun Prog8ANTLRParser.When_choiceContext.toAst(encoding: IStringEncoding): WhenChoice { - val values = expression_list()?.toAst(encoding) - val stmt = statement()?.toAst(encoding) - val stmtBlock = statement_block()?.toAst(encoding)?.toMutableList() ?: mutableListOf() +private fun Prog8ANTLRParser.When_choiceContext.toAst(): WhenChoice { + val values = expression_list()?.toAst() + val stmt = statement()?.toAst() + val stmtBlock = statement_block()?.toAst()?.toMutableList() ?: mutableListOf() if(stmt!=null) stmtBlock.add(stmt) val scope = AnonymousScope(stmtBlock, toPosition()) return WhenChoice(values?.toMutableList(), scope, toPosition()) } -private fun Prog8ANTLRParser.VardeclContext.toAst(encoding: IStringEncoding): VarDecl { +private fun Prog8ANTLRParser.VardeclContext.toAst(): VarDecl { return VarDecl( VarDeclType.VAR, datatype()?.toAst() ?: DataType.UNDEFINED, if(ZEROPAGE() != null) ZeropageWish.PREFER_ZEROPAGE else ZeropageWish.DONTCARE, - arrayindex()?.toAst(encoding), + arrayindex()?.toAst(), varname.text, null, ARRAYSIG() != null || arrayindex() != null, diff --git a/compilerAst/src/prog8/ast/expressions/AstExpressions.kt b/compilerAst/src/prog8/ast/expressions/AstExpressions.kt index 1d6d721c2..d106b7f9a 100644 --- a/compilerAst/src/prog8/ast/expressions/AstExpressions.kt +++ b/compilerAst/src/prog8/ast/expressions/AstExpressions.kt @@ -7,7 +7,6 @@ import prog8.ast.statements.* import prog8.ast.walk.AstWalker import prog8.ast.walk.IAstVisitor import java.util.* -import kotlin.math.abs val associativeOperators = setOf("+", "*", "&", "|", "^", "or", "and", "xor", "==", "!=") @@ -498,6 +497,37 @@ class NumericLiteralValue(val type: DataType, // only numerical types allowed } } +class CharLiteral(val value: Char, + val altEncoding: Boolean, // such as: screencodes instead of Petscii for the C64 + override val position: Position) : Expression() { + override lateinit var parent: Node + + override fun linkParents(parent: Node) { + this.parent = parent + } + + override val isSimple = true + + override fun replaceChildNode(node: Node, replacement: Node) { + throw FatalAstException("can't replace here") + } + + override fun referencesIdentifier(vararg scopedName: String) = false + override fun constValue(program: Program): NumericLiteralValue? = null // TODO: CharLiteral.constValue can't be NumericLiteralValue... + override fun accept(visitor: IAstVisitor) = visitor.visit(this) + override fun accept(walker: AstWalker, parent: Node) = walker.visit(this, parent) + + override fun toString(): String = "'${escape(value.toString())}'" + override fun inferType(program: Program): InferredTypes.InferredType = InferredTypes.knownFor(DataType.UNDEFINED) // FIXME: CharLiteral.inferType + operator fun compareTo(other: CharLiteral): Int = value.compareTo(other.value) + override fun hashCode(): Int = Objects.hash(value, altEncoding) + override fun equals(other: Any?): Boolean { + if (other == null || other !is CharLiteral) + return false + return value == other.value && altEncoding == other.altEncoding + } +} + class StringLiteralValue(val value: String, val altEncoding: Boolean, // such as: screencodes instead of Petscii for the C64 override val position: Position) : Expression() { @@ -636,7 +666,6 @@ class ArrayLiteralValue(val type: InferredTypes.InferredType, // inferred be class RangeExpr(var from: Expression, var to: Expression, var step: Expression, - private val encoding: IStringEncoding, override val position: Position) : Expression() { override lateinit var parent: Node @@ -689,51 +718,8 @@ class RangeExpr(var from: Expression, return "RangeExpr(from $from, to $to, step $step, pos=$position)" } - fun size(): Int? { - val fromLv = (from as? NumericLiteralValue) - val toLv = (to as? NumericLiteralValue) - if(fromLv==null || toLv==null) - return null - return toConstantIntegerRange()?.count() - } - - fun toConstantIntegerRange(): IntProgression? { - val fromVal: Int - val toVal: Int - val fromString = from as? StringLiteralValue - val toString = to as? StringLiteralValue - if(fromString!=null && toString!=null ) { - // string range -> int range over character values - fromVal = encoding.encodeString(fromString.value, fromString.altEncoding)[0].toInt() - toVal = encoding.encodeString(toString.value, fromString.altEncoding)[0].toInt() - } else { - val fromLv = from as? NumericLiteralValue - val toLv = to as? NumericLiteralValue - if(fromLv==null || toLv==null) - return null // non-constant range - // integer range - fromVal = fromLv.number.toInt() - toVal = toLv.number.toInt() - } - val stepVal = (step as? NumericLiteralValue)?.number?.toInt() ?: 1 - return makeRange(fromVal, toVal, stepVal) - } } -internal fun makeRange(fromVal: Int, toVal: Int, stepVal: Int): IntProgression { - return when { - fromVal <= toVal -> when { - stepVal <= 0 -> IntRange.EMPTY - stepVal == 1 -> fromVal..toVal - else -> fromVal..toVal step stepVal - } - else -> when { - stepVal >= 0 -> IntRange.EMPTY - stepVal == -1 -> fromVal downTo toVal - else -> fromVal downTo toVal step abs(stepVal) - } - } -} data class IdentifierReference(val nameInSource: List, override val position: Position) : Expression(), IAssignable { override lateinit var parent: Node diff --git a/compilerAst/src/prog8/ast/walk/AstWalker.kt b/compilerAst/src/prog8/ast/walk/AstWalker.kt index 194b1f2e6..91a10db12 100644 --- a/compilerAst/src/prog8/ast/walk/AstWalker.kt +++ b/compilerAst/src/prog8/ast/walk/AstWalker.kt @@ -110,6 +110,7 @@ abstract class AstWalker { open fun before(untilLoop: UntilLoop, parent: Node): Iterable = noModifications open fun before(returnStmt: Return, parent: Node): Iterable = noModifications open fun before(scope: AnonymousScope, parent: Node): Iterable = noModifications + open fun before(char: CharLiteral, parent: Node): Iterable = noModifications open fun before(string: StringLiteralValue, parent: Node): Iterable = noModifications open fun before(subroutine: Subroutine, parent: Node): Iterable = noModifications open fun before(typecast: TypecastExpression, parent: Node): Iterable = noModifications @@ -150,6 +151,7 @@ abstract class AstWalker { open fun after(untilLoop: UntilLoop, parent: Node): Iterable = noModifications open fun after(returnStmt: Return, parent: Node): Iterable = noModifications open fun after(scope: AnonymousScope, parent: Node): Iterable = noModifications + open fun after(char: CharLiteral, parent: Node): Iterable = noModifications open fun after(string: StringLiteralValue, parent: Node): Iterable = noModifications open fun after(subroutine: Subroutine, parent: Node): Iterable = noModifications open fun after(typecast: TypecastExpression, parent: Node): Iterable = noModifications @@ -300,6 +302,11 @@ abstract class AstWalker { track(after(numLiteral, parent), numLiteral, parent) } + fun visit(char: CharLiteral, parent: Node) { + track(before(char, parent), char, parent) + track(after(char, parent), char, parent) + } + fun visit(string: StringLiteralValue, parent: Node) { track(before(string, parent), string, parent) track(after(string, parent), string, parent) diff --git a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt index a2fcdc673..ca71f94bb 100644 --- a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt +++ b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt @@ -79,6 +79,9 @@ interface IAstVisitor { fun visit(numLiteral: NumericLiteralValue) { } + fun visit(char: CharLiteral) { + } + fun visit(string: StringLiteralValue) { } diff --git a/compilerAst/src/prog8/parser/ModuleParsing.kt b/compilerAst/src/prog8/parser/ModuleParsing.kt deleted file mode 100644 index d71444989..000000000 --- a/compilerAst/src/prog8/parser/ModuleParsing.kt +++ /dev/null @@ -1,173 +0,0 @@ -package prog8.parser - -import org.antlr.v4.runtime.* -import prog8.ast.IStringEncoding -import prog8.ast.Module -import prog8.ast.Program -import prog8.ast.antlr.toAst -import prog8.ast.base.Position -import prog8.ast.base.SyntaxError -import prog8.ast.statements.Directive -import prog8.ast.statements.DirectiveArg -import java.io.InputStream -import java.nio.file.FileSystems -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths - - -class ParsingFailedError(override var message: String) : Exception(message) - -internal class CustomLexer(val modulePath: Path, input: CharStream?) : Prog8ANTLRLexer(input) - -fun moduleName(fileName: Path) = fileName.toString().substringBeforeLast('.') - -internal fun pathFrom(stringPath: String, vararg rest: String): Path = FileSystems.getDefault().getPath(stringPath, *rest) - - -class ModuleImporter(private val program: Program, - private val encoder: IStringEncoding, - private val compilationTargetName: String, - private val libdirs: List) { - - fun importModule(filePath: Path): Module { - print("importing '${moduleName(filePath.fileName)}'") - if(filePath.parent!=null) { - var importloc = filePath.toString() - val curdir = Paths.get("").toAbsolutePath().toString() - if(importloc.startsWith(curdir)) - importloc = "." + importloc.substring(curdir.length) - println(" (from '$importloc')") - } - else - println("") - if(!Files.isReadable(filePath)) - throw ParsingFailedError("No such file: $filePath") - - return importModule(CharStreams.fromPath(filePath), filePath) - } - - fun importLibraryModule(name: String): Module? { - val import = Directive("%import", listOf( - DirectiveArg("", name, 42, position = Position("<<>>", 0, 0, 0)) - ), Position("<<>>", 0, 0, 0)) - return executeImportDirective(import, Paths.get("")) - } - - private class MyErrorListener: ConsoleErrorListener() { - var numberOfErrors: Int = 0 - override fun syntaxError(recognizer: Recognizer<*, *>?, offendingSymbol: Any?, line: Int, charPositionInLine: Int, msg: String, e: RecognitionException?) { - numberOfErrors++ - when (recognizer) { - is CustomLexer -> System.err.println("${recognizer.modulePath}:$line:$charPositionInLine: $msg") - is Prog8ANTLRParser -> System.err.println("${recognizer.inputStream.sourceName}:$line:$charPositionInLine: $msg") - else -> System.err.println("$line:$charPositionInLine $msg") - } - if(numberOfErrors>=5) - throw ParsingFailedError("There are too many parse errors. Stopping.") - } - } - - private fun importModule(stream: CharStream, modulePath: Path): Module { - val moduleName = moduleName(modulePath.fileName) - val lexer = CustomLexer(modulePath, stream) - lexer.removeErrorListeners() - val lexerErrors = MyErrorListener() - lexer.addErrorListener(lexerErrors) - val tokens = CommentHandlingTokenStream(lexer) - val parser = Prog8ANTLRParser(tokens) - parser.removeErrorListeners() - parser.addErrorListener(MyErrorListener()) - val parseTree = parser.module() - val numberOfErrors = parser.numberOfSyntaxErrors + lexerErrors.numberOfErrors - if(numberOfErrors > 0) - throw ParsingFailedError("There are $numberOfErrors errors in '$moduleName'.") - - // You can do something with the parsed comments: - // tokens.commentTokens().forEach { println(it) } - - // convert to Ast - val moduleAst = parseTree.toAst(moduleName, modulePath, encoder) - moduleAst.program = program - moduleAst.linkParents(program.namespace) - program.modules.add(moduleAst) - - // accept additional imports - val lines = moduleAst.statements.toMutableList() - lines.asSequence() - .mapIndexed { i, it -> i to it } - .filter { (it.second as? Directive)?.directive == "%import" } - .forEach { executeImportDirective(it.second as Directive, modulePath) } - - moduleAst.statements = lines - return moduleAst - } - - private fun executeImportDirective(import: Directive, source: Path): Module? { - if(import.directive!="%import" || import.args.size!=1 || import.args[0].name==null) - throw SyntaxError("invalid import directive", import.position) - val moduleName = import.args[0].name!! - if("$moduleName.p8" == import.position.file) - throw SyntaxError("cannot import self", import.position) - - val existing = program.modules.singleOrNull { it.name == moduleName } - if(existing!=null) - return null - - val rsc = tryGetModuleFromResource("$moduleName.p8", compilationTargetName) - val importedModule = - if(rsc!=null) { - // load the module from the embedded resource - val (resource, resourcePath) = rsc - resource.use { - println("importing '$moduleName' (library)") - val content = it.reader().readText().replace("\r\n", "\n") - importModule(CharStreams.fromString(content), Module.pathForResource(resourcePath)) - } - } else { - val modulePath = tryGetModuleFromFile(moduleName, source, import.position) - importModule(modulePath) - } - - removeDirectivesFromImportedModule(importedModule) - return importedModule - } - - private fun removeDirectivesFromImportedModule(importedModule: Module) { - // Most global directives don't apply for imported modules, so remove them - val moduleLevelDirectives = listOf("%output", "%launcher", "%zeropage", "%zpreserved", "%address") - var directives = importedModule.statements.filterIsInstance() - importedModule.statements.removeAll(directives) - directives = directives.filter{ it.directive !in moduleLevelDirectives } - importedModule.statements.addAll(0, directives) - } - - private fun tryGetModuleFromResource(name: String, compilationTargetName: String): Pair? { - val targetSpecificPath = "/prog8lib/$compilationTargetName/$name" - val targetSpecificResource = object{}.javaClass.getResourceAsStream(targetSpecificPath) - if(targetSpecificResource!=null) - return Pair(targetSpecificResource, targetSpecificPath) - - val generalPath = "/prog8lib/$name" - val generalResource = object{}.javaClass.getResourceAsStream(generalPath) - if(generalResource!=null) - return Pair(generalResource, generalPath) - - return null - } - - private fun tryGetModuleFromFile(name: String, source: Path, position: Position?): Path { - val fileName = "$name.p8" - val libpaths = libdirs.map {Path.of(it)} - val locations = - (if(source.toString().isEmpty()) libpaths else libpaths.drop(1) + listOf(source.parent ?: Path.of("."))) + - listOf(Paths.get(Paths.get("").toAbsolutePath().toString(), "prog8lib")) - - locations.forEach { - val file = pathFrom(it.toString(), fileName) - if (Files.isReadable(file)) return file - } - - throw ParsingFailedError("$position Import: no module source file '$fileName' found (I've looked in: embedded libs and $locations)") - } -} diff --git a/compilerAst/src/prog8/parser/Prog8Parser.kt b/compilerAst/src/prog8/parser/Prog8Parser.kt new file mode 100644 index 000000000..1f783fada --- /dev/null +++ b/compilerAst/src/prog8/parser/Prog8Parser.kt @@ -0,0 +1,124 @@ +package prog8.parser + +import org.antlr.v4.runtime.* +import prog8.ast.Module +import prog8.ast.antlr.toAst +import prog8.ast.base.Position +import prog8.ast.statements.Block +import prog8.ast.statements.Directive + + +open class ParsingFailedError(override var message: String) : Exception(message) + +class ParseError(override var message: String, val position: Position, cause: RuntimeException) + : ParsingFailedError("${position.toClickableStr()}$message") { + init { + initCause(cause) + } +} + +object Prog8Parser { + + fun parseModule(src: SourceCode): Module { + val antlrErrorListener = AntlrErrorListener(src) + val lexer = Prog8ANTLRLexer(src.getCharStream()) + lexer.removeErrorListeners() + lexer.addErrorListener(antlrErrorListener) + val tokens = CommonTokenStream(lexer) + val parser = Prog8ANTLRParser(tokens) + parser.errorHandler = Prog8ErrorStrategy + parser.removeErrorListeners() + parser.addErrorListener(antlrErrorListener) + + val parseTree = parser.module() + + val module = ParsedModule(src) + + // .linkParents called in ParsedModule.add + parseTree.directive().forEach { module.add(it.toAst()) } + // TODO: remove Encoding + parseTree.block().forEach { module.add(it.toAst(module.isLibrary())) } + + return module + } + + private class ParsedModule(source: SourceCode) : Module( + // FIXME: hacking together a name for the module: + name = source.pathString() + .substringBeforeLast(".") // must also work with an origin = "" + .substringAfterLast("/") + .substringAfterLast("\\") + .replace("String@", "anonymous_"), + statements = mutableListOf(), + position = Position(source.origin, 1, 0, 0), + source + ) { + val provenance = Pair(source, Triple(1, 0, 0)) + + /** + * Adds a [Directive] to [statements] and + * sets this Module as its [parent]. + * Note: you can only add [Directive]s or [Block]s to a Module. + */ + fun add(child: Directive) { + child.linkParents(this) + statements.add(child) + } + /** + * Adds a [Block] to [statements] and + * sets this Module as its [parent]. + * Note: you can only add [Directive]s or [Block]s to a Module. + */ + fun add(child: Block) { + child.linkParents(this) + statements.add(child) + } + } + + private object Prog8ErrorStrategy: BailErrorStrategy() { + private fun fillIn(e: RecognitionException?, ctx: ParserRuleContext?) { + var context = ctx + while (context != null) { + context.exception = e + context = context.getParent() + } + } + + override fun reportInputMismatch(recognizer: Parser?, e: InputMismatchException?) { + super.reportInputMismatch(recognizer, e) + } + + override fun recover(recognizer: Parser?, e: RecognitionException?) { + fillIn(e, recognizer!!.context) + reportError(recognizer, e) + } + + override fun recoverInline(recognizer: Parser?): Token { + val e = InputMismatchException(recognizer) + fillIn(e, recognizer!!.context) + reportError(recognizer, e) + throw e + } + } + + private class AntlrErrorListener(val src: SourceCode): BaseErrorListener() { + override fun syntaxError(recognizer: Recognizer<*, *>?, offendingSymbol: Any?, line: Int, charPositionInLine: Int, msg: String, e: RecognitionException?) { + if (e == null) { + TODO("no RecognitionException - create your own ParseError") + //throw ParseError() + } else { + throw ParseError(msg, e.getPosition(src.origin), e) + } + } + } + + private fun RecognitionException.getPosition(file: String) : Position { + val offending = this.offendingToken + val line = offending.line + val beginCol = offending.charPositionInLine + val endCol = beginCol + offending.stopIndex - offending.startIndex // TODO: point to col *after* token? + val pos = Position(file, line, beginCol, endCol) + return pos + } + +} diff --git a/compilerAst/src/prog8/parser/SourceCode.kt b/compilerAst/src/prog8/parser/SourceCode.kt new file mode 100644 index 000000000..57d1a7724 --- /dev/null +++ b/compilerAst/src/prog8/parser/SourceCode.kt @@ -0,0 +1,140 @@ +package prog8.parser + +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.CharStreams +import java.io.File +import java.nio.file.Path +import kotlin.io.path.* + +/** + * Encapsulates - and ties together - actual source code (=text) + * and its [origin]. + */ +abstract class SourceCode { + + /** + * To be used *only* by the parser (as input to a TokenStream). + * DO NOT mess around with! + */ + internal abstract fun getCharStream(): CharStream + + /** + * Whether this [SourceCode] instance was created by + * factory method [fromResources] + */ + abstract val isFromResources: Boolean + + /** + * Where this [SourceCode] instance came from. + * This can be one of the following: + * * a normal string representation of a [java.nio.file.Path], if it originates from a file (see [fromPath]) + * * `` if was created via [of] + * * `@embedded@/x/y/z.ext` if it came from resources (see [fromResources]) + */ + abstract val origin: String + + + /** + * FIXME: hacking together a [SourceCode]'s "path string" + * This is really just [origin] with any stuff removed that would render it an invalid path name. + * (Note: a *valid* path name does NOT mean that the denoted file or folder *exists*) + */ + fun pathString() = + origin + .substringAfter("<").substringBeforeLast(">") // or from plain string? + + /** + * The source code as plain string. + * *Note: this is meant for testing and debugging, do NOT use in application code!* + */ + fun asString() = this.getCharStream().toString() + + /** + * Deliberately does NOT return the actual text. + * For this - if at all - use [getCharStream]. + */ + final override fun toString() = "${this.javaClass.name}[${this.origin}]" + + // "static" factory methods + companion object { + + /** + * Turn a plain String into a [SourceCode] object. + * [origin] will be something like ``. + */ + fun of(text: String): SourceCode { + return object : SourceCode() { + override val isFromResources = false + override val origin = "" + override fun getCharStream(): CharStream { + return CharStreams.fromString(text) + } + } + } + + /** + * Get [SourceCode] from the file represented by the specified Path. + * This does not actually *access* the file, but it does check + * whether it + * * exists + * * is a regular file (ie: not a directory) + * * and is actually readable + * + * [origin] will be the given path in absolute and normalized form. + * @throws NoSuchFileException if the file does not exist + * @throws AccessDeniedException if the given path points to a directory or the file is non-readable for some other reason + */ + fun fromPath(path: Path): SourceCode { + val normalized = path.normalize() + val file = normalized.toFile() + if (!path.exists()) + throw NoSuchFileException(file) + if (path.isDirectory()) + throw AccessDeniedException(file, reason = "Not a file but a directory") + if (!path.isReadable()) + throw AccessDeniedException(file, reason = "Is not readable") + return object : SourceCode() { + override val isFromResources = false + override val origin = normalized.absolutePathString() + override fun getCharStream(): CharStream { + return CharStreams.fromPath(normalized) + } + } + } + + /** + * [origin]: `` for a given `pathString` of "x/y/z.p8" + */ + fun fromResources(pathString: String): SourceCode { + val path = Path.of(pathString).normalize() + val sep = "/" + val normalized = sep + path.toMutableList().joinToString(sep) + val rscURL = object{}.javaClass.getResource(normalized) + if (rscURL == null) { + val rscRoot = object{}.javaClass.getResource("/") + throw NoSuchFileException( + File(normalized), + reason = "looked in resources rooted at $rscRoot") + } + return object : SourceCode() { + override val isFromResources = true + override val origin = "@embedded@$normalized" + override fun getCharStream(): CharStream { + val inpStr = object{}.javaClass.getResourceAsStream(normalized) + val chars = CharStreams.fromStream(inpStr) + return chars + } + } + } + + // TODO: possibly more, like fromURL(..) +/* // For `jar:..` URLs + // see https://stackoverflow.com/questions/22605666/java-access-files-in-jar-causes-java-nio-file-filesystemnotfoundexception + var url = URL("jar:file:/E:/x16/prog8(meisl)/compiler/build/libs/prog8compiler-7.0-BETA3-all.jar!/prog8lib/c64/textio.p8") + val uri = url.toURI() + val parts = uri.toString().split("!") + val fs = FileSystems.newFileSystem(URI.create(parts[0]), mutableMapOf(Pair("", "")) ) + val path = fs.getPath(parts[1]) +*/ + } +} diff --git a/compilerAst/test/TestAntlrParser.kt b/compilerAst/test/TestAntlrParser.kt deleted file mode 100644 index 085917bbb..000000000 --- a/compilerAst/test/TestAntlrParser.kt +++ /dev/null @@ -1,259 +0,0 @@ -package prog8tests - -import org.antlr.v4.runtime.* -import org.antlr.v4.runtime.misc.ParseCancellationException -import org.junit.jupiter.api.Test -import prog8.ast.IBuiltinFunctions -import prog8.ast.IMemSizer -import prog8.ast.IStringEncoding -import prog8.ast.antlr.toAst -import prog8.ast.base.DataType -import prog8.ast.base.Position -import prog8.ast.expressions.Expression -import prog8.ast.expressions.InferredTypes -import prog8.ast.expressions.NumericLiteralValue -import prog8.ast.statements.Block -import prog8.parser.ParsingFailedError -import prog8.parser.Prog8ANTLRLexer -import prog8.parser.Prog8ANTLRParser -import java.nio.file.Path -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertIs - -class TestAntlrParser { - - class MyErrorListener: ConsoleErrorListener() { - override fun syntaxError(recognizer: Recognizer<*, *>?, offendingSymbol: Any?, line: Int, charPositionInLine: Int, msg: String, e: RecognitionException?) { - throw ParsingFailedError("line $line:$charPositionInLine $msg") - } - } - - class MyErrorStrategy: BailErrorStrategy() { - override fun recover(recognizer: Parser?, e: RecognitionException?) { - try { - // let it fill in e in all the contexts - super.recover(recognizer, e) - } catch (pce: ParseCancellationException) { - reportError(recognizer, e) - } - } - - override fun recoverInline(recognizer: Parser?): Token { - throw InputMismatchException(recognizer) - } - } - - private fun parseModule(srcText: String): Prog8ANTLRParser.ModuleContext { - return parseModule(CharStreams.fromString(srcText)) - } - - private fun parseModule(srcFile: Path): Prog8ANTLRParser.ModuleContext { - return parseModule(CharStreams.fromPath(srcFile)) - } - - private fun parseModule(srcStream: CharStream): Prog8ANTLRParser.ModuleContext { - val errorListener = MyErrorListener() - val lexer = Prog8ANTLRLexer(srcStream) - lexer.removeErrorListeners() - lexer.addErrorListener(errorListener) - val tokens = CommonTokenStream(lexer) - val parser = Prog8ANTLRParser(tokens) - parser.errorHandler = MyErrorStrategy() - parser.removeErrorListeners() - parser.addErrorListener(errorListener) - return parser.module() - } - - object DummyEncoding: IStringEncoding { - override fun encodeString(str: String, altEncoding: Boolean): List { - TODO("Not yet implemented") - } - - override fun decodeString(bytes: List, altEncoding: Boolean): String { - TODO("Not yet implemented") - } - } - - object DummyFunctions: IBuiltinFunctions { - override val names: Set = emptySet() - override val purefunctionNames: Set = emptySet() - override fun constValue(name: String, args: List, position: Position, memsizer: IMemSizer): NumericLiteralValue? = null - override fun returnType(name: String, args: MutableList) = InferredTypes.InferredType.unknown() - } - - object DummyMemsizer: IMemSizer { - override fun memorySize(dt: DataType): Int = 0 - } - - @Test - fun testModuleSourceNeedNotEndWithNewline() { - val nl = "\n" // say, Unix-style (different flavours tested elsewhere) - val srcText = "foo {" + nl + "}" // source ends with '}' (= NO newline, issue #40) - - // before the fix, prog8Parser would have reported (thrown) "missing at ''" - val parseTree = parseModule(srcText) - assertEquals(parseTree.block().size, 1) - } - - @Test - fun testModuleSourceMayEndWithNewline() { - val nl = "\n" // say, Unix-style (different flavours tested elsewhere) - val srcText = "foo {" + nl + "}" + nl // source does end with a newline (issue #40) - val parseTree = parseModule(srcText) - assertEquals(parseTree.block().size, 1) - } - - @Test - fun testAllBlocksButLastMustEndWithNewline() { - val nl = "\n" // say, Unix-style (different flavours tested elsewhere) - - // BAD: 2nd block `bar` does NOT start on new line; however, there's is a nl at the very end - val srcBad = "foo {" + nl + "}" + " bar {" + nl + "}" + nl - - // GOOD: 2nd block `bar` does start on a new line; however, a nl at the very end ain't needed - val srcGood = "foo {" + nl + "}" + nl + "bar {" + nl + "}" - - assertFailsWith { parseModule(srcBad) } - val parseTree = parseModule(srcGood) - assertEquals(parseTree.block().size, 2) - } - - @Test - fun testWindowsAndMacNewlinesAreAlsoFine() { - val nlWin = "\r\n" - val nlUnix = "\n" - val nlMac = "\r" - - //parseModule(Paths.get("test", "fixtures", "mac_newlines.p8").toAbsolutePath()) - - // a good mix of all kinds of newlines: - val srcText = - "foo {" + - nlMac + - nlWin + - "}" + - nlMac + // <-- do test a single \r (!) where an EOL is expected - "bar {" + - nlUnix + - "}" + - nlUnix + nlMac // both should be "eaten up" by just one EOL token - "combi {" + - nlMac + nlWin + nlUnix // all three should be "eaten up" by just one EOL token - "}" + - nlUnix // end with newline (see testModuleSourceNeedNotEndWithNewline) - - val parseTree = parseModule(srcText) - assertEquals(parseTree.block().size, 2) - } - - @Test - fun testInterleavedEolAndCommentBeforeFirstBlock() { - // issue: #47 - val srcText = """ - ; comment - - ; comment - - blockA { - } -""" - val parseTree = parseModule(srcText) - assertEquals(parseTree.block().size, 1) - } - - @Test - fun testInterleavedEolAndCommentBetweenBlocks() { - // issue: #47 - val srcText = """ - blockA { - } - ; comment - - ; comment - - blockB { - } -""" - val parseTree = parseModule(srcText) - assertEquals(parseTree.block().size, 2) - } - - @Test - fun testInterleavedEolAndCommentAfterLastBlock() { - // issue: #47 - val srcText = """ - blockA { - } - ; comment - - ; comment - -""" - val parseTree = parseModule(srcText) - assertEquals(parseTree.block().size, 1) - } - - @Test - fun testNewlineBetweenTwoBlocksOrDirectivesStillRequired() { - // issue: #47 - - // block and block - assertFailsWith{ parseModule(""" - blockA { - } blockB { - } - """) } - - // block and directive - assertFailsWith{ parseModule(""" - blockB { - } %import textio - """) } - - // The following two are bogus due to directive *args* expected to follow the directive name. - // Leaving them in anyways. - - // dir and block - assertFailsWith{ parseModule(""" - %import textio blockB { - } - """) } - - assertFailsWith{ parseModule(""" - %import textio %import syslib - """) } - } - - /* - @Test - fun testImportLibraryModule() { - val program = Program("foo", mutableListOf(), DummyFunctions, DummyMemsizer) - val importer = ModuleImporter(program, DummyEncoding, "blah", listOf("./test/fixtures")) - - //assertFailsWith(){ importer.importLibraryModule("import_file_with_syntax_error") } - } - */ - - @Test - fun testProg8Ast() { - // can create charstreams from many other sources as well; - val charstream = CharStreams.fromString(""" -main { - sub start() { - return - } -} -""") - val lexer = Prog8ANTLRLexer(charstream) - val tokens = CommonTokenStream(lexer) - val parser = Prog8ANTLRParser(tokens) - parser.errorHandler = BailErrorStrategy() -// parser.removeErrorListeners() -// parser.addErrorListener(MyErrorListener()) - - val ast = parser.module().toAst("test", Path.of(""), DummyEncoding) - assertIs(ast.statements.first()) - assertEquals((ast.statements.first() as Block).name, "main") - } -} diff --git a/compilerAst/test/TestAstToSourceCode.kt b/compilerAst/test/TestAstToSourceCode.kt new file mode 100644 index 000000000..b179b48b9 --- /dev/null +++ b/compilerAst/test/TestAstToSourceCode.kt @@ -0,0 +1,115 @@ +package prog8tests + +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Disabled +import kotlin.test.* +import prog8tests.helpers.* + +import prog8.ast.* +import prog8.parser.Prog8Parser.parseModule +import prog8.parser.SourceCode + +import prog8.ast.AstToSourceCode +import prog8.parser.ParseError + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestAstToSourceCode { + + private fun generateP8(module: Module) : String { + val program = Program("test", DummyFunctions, DummyMemsizer) + .addModule(module) + + var generatedText = "" + val it = AstToSourceCode({ str -> generatedText += str }, program) + it.visit(program) + + return generatedText + } + + private fun roundTrip(module: Module): Pair { + val generatedText = generateP8(module) + try { + val parsedAgain = parseModule(SourceCode.of(generatedText)) + return Pair(generatedText, parsedAgain) + } catch (e: ParseError) { + assert(false) { "should produce valid Prog8 but threw $e" } + throw e + } + } + + @Test + fun testMentionsInternedStringsModule() { + val orig = SourceCode.of("\n") + val (txt, _) = roundTrip(parseModule(orig)) + // assertContains has *actual* first! + assertContains(txt, Regex(";.*$internedStringsModuleName")) + } + + @Test + fun testImportDirectiveWithLib() { + val orig = SourceCode.of("%import textio\n") + val (txt, _) = roundTrip(parseModule(orig)) + // assertContains has *actual* first! + assertContains(txt, Regex("%import +textio")) + } + + @Test + fun testImportDirectiveWithUserModule() { + val orig = SourceCode.of("%import my_own_stuff\n") + val (txt, _) = roundTrip(parseModule(orig)) + // assertContains has *actual* first! + assertContains(txt, Regex("%import +my_own_stuff")) + } + + + @Test + fun testStringLiteral_noAlt() { + val orig = SourceCode.of(""" + main { + str s = "fooBar\n" + } + """) + val (txt, _) = roundTrip(parseModule(orig)) + // assertContains has *actual* first! + assertContains(txt, Regex("str +s += +\"fooBar\\\\n\"")) + } + + @Test + fun testStringLiteral_withAlt() { + val orig = SourceCode.of(""" + main { + str sAlt = @"fooBar\n" + } + """) + val (txt, _) = roundTrip(parseModule(orig)) + // assertContains has *actual* first! + assertContains(txt, Regex("str +sAlt += +@\"fooBar\\\\n\"")) + } + + @Test + fun testCharLiteral_noAlt() { + val orig = SourceCode.of(""" + main { + ubyte c = 'x' + } + """) + val (txt, _) = roundTrip(parseModule(orig)) + // assertContains has *actual* first! + assertContains(txt, Regex("ubyte +c += +'x'"), "char literal") + } + + @Test + fun testCharLiteral_withAlt() { + val orig = SourceCode.of(""" + main { + ubyte cAlt = @'x' + } + """) + val (txt, _) = roundTrip(parseModule(orig)) + // assertContains has *actual* first! + assertContains(txt, Regex("ubyte +cAlt += +@'x'"), "alt char literal") + } + +} diff --git a/compilerAst/test/TestProg8Parser.kt b/compilerAst/test/TestProg8Parser.kt new file mode 100644 index 000000000..d30abcf23 --- /dev/null +++ b/compilerAst/test/TestProg8Parser.kt @@ -0,0 +1,527 @@ +package prog8tests + +import prog8tests.helpers.* +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested +import kotlin.test.* +import kotlin.io.path.* + +import prog8.parser.ParseError +import prog8.parser.Prog8Parser.parseModule +import prog8.parser.SourceCode +import prog8.ast.* +import prog8.ast.statements.* +import prog8.ast.base.Position +import prog8.ast.expressions.* + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestProg8Parser { + + @Nested + inner class Newline { + + @Nested + inner class AtEnd { + + @Test + fun `is not required - #40, fixed by #45`() { + val nl = "\n" // say, Unix-style (different flavours tested elsewhere) + val src = SourceCode.of("foo {" + nl + "}") // source ends with '}' (= NO newline, issue #40) + + // #40: Prog8ANTLRParser would report (throw) "missing at ''" + val module = parseModule(src) + assertEquals(1, module.statements.size) + } + + @Test + fun `is still accepted - #40, fixed by #45`() { + val nl = "\n" // say, Unix-style (different flavours tested elsewhere) + val srcText = "foo {" + nl + "}" + nl // source does end with a newline (issue #40) + val module = parseModule(SourceCode.of(srcText)) + assertEquals(1, module.statements.size) + } + } + + @Test + fun `is required after each block except the last`() { + val nl = "\n" // say, Unix-style (different flavours tested elsewhere) + + // BAD: 2nd block `bar` does NOT start on new line; however, there's is a nl at the very end + val srcBad = "foo {" + nl + "}" + " bar {" + nl + "}" + nl + + // GOOD: 2nd block `bar` does start on a new line; however, a nl at the very end ain't needed + val srcGood = "foo {" + nl + "}" + nl + "bar {" + nl + "}" + + assertFailsWith { parseModule(SourceCode.of(srcBad)) } + val module = parseModule(SourceCode.of(srcGood)) + assertEquals(2, module.statements.size) + } + + @Test + fun `is required between two Blocks or Directives - #47`() { + // block and block + assertFailsWith{ parseModule(SourceCode.of(""" + blockA { + } blockB { + } + """)) } + + // block and directive + assertFailsWith{ parseModule(SourceCode.of(""" + blockB { + } %import textio + """)) } + + // The following two are bogus due to directive *args* expected to follow the directive name. + // Leaving them in anyways. + + // dir and block + assertFailsWith{ parseModule(SourceCode.of(""" + %import textio blockB { + } + """)) } + + assertFailsWith{ parseModule(SourceCode.of(""" + %import textio %import syslib + """)) } + } + + @Test + fun `can be Win, Unix or mixed, even mixed`() { + val nlWin = "\r\n" + val nlUnix = "\n" + val nlMac = "\r" + + //parseModule(Paths.get("test", "fixtures", "mac_newlines.p8").toAbsolutePath()) + + // a good mix of all kinds of newlines: + val srcText = + "foo {" + + nlMac + + nlWin + + "}" + + nlMac + // <-- do test a single \r (!) where an EOL is expected + "bar {" + + nlUnix + + "}" + + nlUnix + nlMac // both should be "eaten up" by just one EOL token + "combi {" + + nlMac + nlWin + nlUnix // all three should be "eaten up" by just one EOL token + "}" + + nlUnix // end with newline (see testModuleSourceNeedNotEndWithNewline) + + val module = parseModule(SourceCode.of(srcText)) + assertEquals(2, module.statements.size) + } + } + + @Nested + inner class EOLsInterleavedWithComments { + + @Test + fun `are ok before first block - #47`() { + // issue: #47 + val srcText = """ + ; comment + + ; comment + + blockA { + } + """ + val module = parseModule(SourceCode.of(srcText)) + assertEquals(1, module.statements.size) + } + + @Test + fun `are ok between blocks - #47`() { + // issue: #47 + val srcText = """ + blockA { + } + ; comment + + ; comment + + blockB { + } + """ + val module = parseModule(SourceCode.of(srcText)) + assertEquals(2, module.statements.size) + } + + @Test + fun `are ok after last block - #47`() { + // issue: #47 + val srcText = """ + blockA { + } + ; comment + + ; comment + + """ + val module = parseModule(SourceCode.of(srcText)) + assertEquals(1, module.statements.size) + } + } + + + @Nested + inner class ImportDirectives { + @Test + fun `should not be looked into by the parser`() { + val importedNoExt = assumeNotExists(fixturesDir, "i_do_not_exist") + assumeNotExists(fixturesDir, "i_do_not_exist.p8") + val text = "%import ${importedNoExt.name}" + val module = parseModule(SourceCode.of(text)) + + assertEquals(1, module.statements.size) + } + } + + + @Nested + inner class EmptySourcecode { + @Test + fun `from an empty string should result in empty Module`() { + val module = parseModule(SourceCode.of("")) + assertEquals(0, module.statements.size) + } + + @Test + fun `from an empty file should result in empty Module`() { + val path = assumeReadableFile(fixturesDir, "empty.p8") + val module = parseModule(SourceCode.fromPath(path)) + assertEquals(0, module.statements.size) + } + } + + @Nested + inner class NameOfModule { + @Test + fun `parsed from a string`() { + val srcText = """ + main { + } + """.trimIndent() + val module = parseModule(SourceCode.of(srcText)) + + // Note: assertContains has *actual* as first param + assertContains(module.name, Regex("^anonymous_[0-9a-f]+$")) + } + + @Test + fun `parsed from a file`() { + val path = assumeReadableFile(fixturesDir, "simple_main.p8") + val module = parseModule(SourceCode.fromPath(path)) + assertEquals(path.nameWithoutExtension, module.name) + } + } + + @Nested + inner class PositionOfAstNodesAndParseErrors { + + private fun assertPosition( + actual: Position, + expFile: String? = null, + expLine: Int? = null, + expStartCol: Int? = null, + expEndCol: Int? = null + ) { + require(!listOf(expLine, expStartCol, expEndCol).all { it == null }) + if (expLine != null) assertEquals(expLine, actual.line, ".position.line (1-based)") + if (expStartCol != null) assertEquals(expStartCol, actual.startCol, ".position.startCol (0-based)") + if (expEndCol != null) assertEquals(expEndCol, actual.endCol, ".position.endCol (0-based)") + if (expFile != null) assertEquals(expFile, actual.file, ".position.file") + } + + private fun assertPosition( + actual: Position, + expFile: Regex? = null, + expLine: Int? = null, + expStartCol: Int? = null, + expEndCol: Int? = null + ) { + require(!listOf(expLine, expStartCol, expEndCol).all { it == null }) + if (expLine != null) assertEquals(expLine, actual.line, ".position.line (1-based)") + if (expStartCol != null) assertEquals(expStartCol, actual.startCol, ".position.startCol (0-based)") + if (expEndCol != null) assertEquals(expEndCol, actual.endCol, ".position.endCol (0-based)") + // Note: assertContains expects *actual* value first + if (expFile != null) assertContains(actual.file, expFile, ".position.file") + } + + private fun assertPositionOf( + actual: Node, + expFile: String? = null, + expLine: Int? = null, + expStartCol: Int? = null, + expEndCol: Int? = null + ) = + assertPosition(actual.position, expFile, expLine, expStartCol, expEndCol) + + private fun assertPositionOf( + actual: Node, + expFile: Regex? = null, + expLine: Int? = null, + expStartCol: Int? = null, + expEndCol: Int? = null + ) = + assertPosition(actual.position, expFile, expLine, expStartCol, expEndCol) + + + @Test + fun `in ParseError from bad string source code`() { + val srcText = "bad * { }\n" + + assertFailsWith { parseModule(SourceCode.of(srcText)) } + try { + parseModule(SourceCode.of(srcText)) + } catch (e: ParseError) { + assertPosition(e.position, Regex("^$"), 1, 4, 4) + } + } + + @Test + fun `in ParseError from bad file source code`() { + val path = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8") + + assertFailsWith { parseModule(SourceCode.fromPath(path)) } + try { + parseModule(SourceCode.fromPath(path)) + } catch (e: ParseError) { + assertPosition(e.position, path.absolutePathString(), 2, 6) // TODO: endCol wrong + } + } + + @Test + fun `of Module parsed from a string`() { + val srcText = """ + main { + } + """.trimIndent() + val module = parseModule(SourceCode.of(srcText)) + assertPositionOf(module, Regex("^$"), 1, 0) // TODO: endCol wrong + } + + @Test + fun `of Module parsed from a file`() { + val path = assumeReadableFile(fixturesDir, "simple_main.p8") + + val module = parseModule(SourceCode.fromPath(path)) + assertPositionOf(module, path.absolutePathString(), 1, 0) // TODO: endCol wrong + } + + @Test + fun `of non-root Nodes parsed from file`() { + val path = assumeReadableFile(fixturesDir, "simple_main.p8") + + val module = parseModule(SourceCode.fromPath(path)) + val mpf = module.position.file + + assertPositionOf(module, path.absolutePathString(), 1, 0) // TODO: endCol wrong + val mainBlock = module.statements.filterIsInstance()[0] + assertPositionOf(mainBlock, mpf, 1, 0) // TODO: endCol wrong! + val startSub = mainBlock.statements.filterIsInstance()[0] + assertPositionOf(startSub, mpf, 2, 4) // TODO: endCol wrong! + } + + + /** + * TODO: this test is testing way too much at once + */ + @Test + @Disabled("TODO: fix .position of nodes below Module - step 8, 'refactor AST gen'") + fun `of non-root Nodes parsed from a string`() { + val srcText = """ + %target 16, "abc" ; DirectiveArg directly inherits from Node - neither an Expression nor a Statement..? + main { + sub start() { + ubyte foo = 42 + ubyte bar + when (foo) { + 23 -> bar = 'x' ; WhenChoice, also directly inheriting Node + 42 -> bar = 'y' + else -> bar = 'z' + } + } + } + """.trimIndent() + val module = parseModule(SourceCode.of(srcText)) + val mpf = module.position.file + + val targetDirective = module.statements.filterIsInstance()[0] + assertPositionOf(targetDirective, mpf, 1, 0) // TODO: endCol wrong! + val mainBlock = module.statements.filterIsInstance()[0] + assertPositionOf(mainBlock, mpf, 2, 0) // TODO: endCol wrong! + val startSub = mainBlock.statements.filterIsInstance()[0] + assertPositionOf(startSub, mpf, 3, 4) // TODO: endCol wrong! + val declFoo = startSub.statements.filterIsInstance()[0] + assertPositionOf(declFoo, mpf, 4, 8) // TODO: endCol wrong! + val rhsFoo = declFoo.value!! + assertPositionOf(rhsFoo, mpf, 4, 20) // TODO: endCol wrong! + val declBar = startSub.statements.filterIsInstance()[1] + assertPositionOf(declBar, mpf, 5, 8) // TODO: endCol wrong! + val whenStmt = startSub.statements.filterIsInstance()[0] + assertPositionOf(whenStmt, mpf, 6, 8) // TODO: endCol wrong! + assertPositionOf(whenStmt.choices[0], mpf, 7, 12) // TODO: endCol wrong! + assertPositionOf(whenStmt.choices[1], mpf, 8, 12) // TODO: endCol wrong! + assertPositionOf(whenStmt.choices[2], mpf, 9, 12) // TODO: endCol wrong! + } + } + + @Nested + inner class CharLiterals { + + @Test + fun `in argument position, no altEnc`() { + val src = SourceCode.of(""" + main { + sub start() { + chrout('\n') + } + } + """) + val module = parseModule(src) + + val startSub = module + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + val funCall = startSub.statements.filterIsInstance().first() + + assertIs(funCall.args[0]) + val char = funCall.args[0] as CharLiteral + assertEquals('\n', char.value) + } + + @Test + fun `on rhs of block-level var decl, no AltEnc`() { + val src = SourceCode.of(""" + main { + ubyte c = 'x' + } + """) + val module = parseModule(src) + val decl = module + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + + val rhs = decl.value as CharLiteral + assertEquals('x', rhs.value, "char literal's .value") + assertEquals(false, rhs.altEncoding, "char literal's .altEncoding") + } + + @Test + fun `on rhs of block-level const decl, with AltEnc`() { + val src = SourceCode.of(""" + main { + const ubyte c = @'x' + } + """) + val module = parseModule(src) + val decl = module + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + + val rhs = decl.value as CharLiteral + assertEquals('x', rhs.value, "char literal's .value") + assertEquals(true, rhs.altEncoding, "char literal's .altEncoding") + } + + @Test + fun `on rhs of subroutine-level var decl, no AltEnc`() { + val src = SourceCode.of(""" + main { + sub start() { + ubyte c = 'x' + } + } + """) + val module = parseModule(src) + val decl = module + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + + val rhs = decl.value as CharLiteral + assertEquals('x', rhs.value, "char literal's .value") + assertEquals(false, rhs.altEncoding, "char literal's .altEncoding") + } + + @Test + fun `on rhs of subroutine-level const decl, with AltEnc`() { + val src = SourceCode.of(""" + main { + sub start() { + const ubyte c = @'x' + } + } + """) + val module = parseModule(src) + val decl = module + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + + val rhs = decl.value as CharLiteral + assertEquals('x', rhs.value, "char literal's .value") + assertEquals(true, rhs.altEncoding, "char literal's .altEncoding") + } + } + + @Nested + inner class Ranges { + + @Test + fun `in for-loops`() { + val module = parseModule(SourceCode.of(""" + main { + sub start() { + ubyte ub + for ub in "start" downto "end" { ; #0 + } + for ub in "something" { ; #1 + } + for ub in @'a' to 'f' { ; #2 + } + for ub in false to true { ; #3 + } + for ub in 9 to 1 { ; #4 - yes, *parser* should NOT check! + } + } + } + """)) + val iterables = module + .statements.filterIsInstance()[0] + .statements.filterIsInstance()[0] + .statements.filterIsInstance() + .map { it.iterable } + + assertEquals(5, iterables.size) + + val it0 = iterables[0] as RangeExpr + assertIs(it0.from, "parser should leave it as is") + assertIs(it0.to, "parser should leave it as is") + + val it1 = iterables[1] as StringLiteralValue + assertEquals("something", it1.value, "parser should leave it as is") + + val it2 = iterables[2] as RangeExpr + assertIs(it2.from, "parser should leave it as is") + assertIs(it2.to, "parser should leave it as is") + + val it3 = iterables[3] as RangeExpr + // TODO: intro BoolLiteral + assertIs(it3.from, "parser should leave it as is") + assertIs(it3.to, "parser should leave it as is") + + val it4 = iterables[4] as RangeExpr + assertIs(it4.from, "parser should leave it as is") + assertIs(it4.to, "parser should leave it as is") + } + } + +} diff --git a/compilerAst/test/TestSourceCode.kt b/compilerAst/test/TestSourceCode.kt new file mode 100644 index 000000000..13ce07fe1 --- /dev/null +++ b/compilerAst/test/TestSourceCode.kt @@ -0,0 +1,146 @@ +package prog8tests + +import prog8tests.helpers.* +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.* +import kotlin.test.* +import kotlin.io.path.* + +import prog8.parser.SourceCode + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestSourceCode { + + @Test + fun testFactoryMethod_Of() { + val text = """ + main { } + """.trimIndent() + val src = SourceCode.of(text) + val actualText = src.getCharStream().toString() + + assertContains(src.origin, Regex("^$")) + assertEquals(text, actualText) + } + + @Test + fun testFromPathWithNonExistingPath() { + val filename = "i_do_not_exist.p8" + val path = assumeNotExists(fixturesDir, filename) + assertFailsWith { SourceCode.fromPath(path) } + } + + @Test + fun testFromPathWithMissingExtension_p8() { + val pathWithoutExt = assumeNotExists(fixturesDir,"simple_main") + assumeReadableFile(fixturesDir,"simple_main.p8") + assertFailsWith { SourceCode.fromPath(pathWithoutExt) } + } + + @Test + fun testFromPathWithDirectory() { + assertFailsWith { SourceCode.fromPath(fixturesDir) } + } + + @Test + fun testFromPathWithExistingPath() { + val filename = "simple_main.p8" + val path = assumeReadableFile(fixturesDir, filename) + val src = SourceCode.fromPath(path) + + val expectedOrigin = path.normalize().absolutePathString() + assertEquals(expectedOrigin, src.origin) + assertEquals(path.toFile().readText(), src.asString()) + } + + @Test + fun testFromPathWithExistingNonNormalizedPath() { + val filename = "simple_main.p8" + val path = Path(".", "test", "..", "test", "fixtures", filename) + val srcFile = assumeReadableFile(path).toFile() + val src = SourceCode.fromPath(path) + + val expectedOrigin = path.normalize().absolutePathString() + assertEquals(expectedOrigin, src.origin) + assertEquals(srcFile.readText(), src.asString()) + } + + @Test + fun testFromResourcesWithExistingP8File_withoutLeadingSlash() { + val pathString = "prog8lib/math.p8" + val srcFile = assumeReadableFile(resourcesDir, pathString).toFile() + val src = SourceCode.fromResources(pathString) + + assertEquals("@embedded@/$pathString", src.origin) + assertEquals(srcFile.readText(), src.asString()) + } + + @Test + fun testFromResourcesWithExistingP8File_withLeadingSlash() { + val pathString = "/prog8lib/math.p8" + val srcFile = assumeReadableFile(resourcesDir, pathString.substring(1)).toFile() + val src = SourceCode.fromResources(pathString) + + assertEquals("@embedded@$pathString", src.origin) + assertEquals(srcFile.readText(), src.asString()) + } + + @Test + fun testFromResourcesWithExistingAsmFile_withoutLeadingSlash() { + val pathString = "prog8lib/math.asm" + val srcFile = assumeReadableFile(resourcesDir, pathString).toFile() + val src = SourceCode.fromResources(pathString) + + assertEquals("@embedded@/$pathString", src.origin) + assertEquals(srcFile.readText(), src.asString()) + assertTrue(src.isFromResources, ".isFromResources") + } + + @Test + fun testFromResourcesWithExistingAsmFile_withLeadingSlash() { + val pathString = "/prog8lib/math.asm" + val srcFile = assumeReadableFile(resourcesDir, pathString.substring(1)).toFile() + val src = SourceCode.fromResources(pathString) + + assertEquals("@embedded@$pathString", src.origin) + assertEquals(srcFile.readText(), src.asString()) + } + + @Test + fun testFromResourcesWithNonNormalizedPath() { + val pathString = "/prog8lib/../prog8lib/math.p8" + val srcFile = assumeReadableFile(resourcesDir, pathString.substring(1)).toFile() + val src = SourceCode.fromResources(pathString) + + assertEquals("@embedded@/prog8lib/math.p8", src.origin) + assertEquals(srcFile.readText(), src.asString()) + assertTrue(src.isFromResources, ".isFromResources") + } + + + @Test + fun testFromResourcesWithNonExistingFile_withLeadingSlash() { + val pathString = "/prog8lib/i_do_not_exist" + assumeNotExists(resourcesDir, pathString.substring(1)) + + assertThrows { SourceCode.fromResources(pathString) } + } + @Test + fun testFromResourcesWithNonExistingFile_withoutLeadingSlash() { + val pathString = "prog8lib/i_do_not_exist" + assumeNotExists(resourcesDir, pathString) + + assertThrows { SourceCode.fromResources(pathString) } + } + + @Test + @Disabled("TODO: inside resources: cannot tell apart a folder from a file") + fun testFromResourcesWithDirectory() { + val pathString = "/prog8lib" + assumeDirectory(resourcesDir, pathString.substring(1)) + assertThrows { SourceCode.fromResources(pathString) } + } + +} diff --git a/compilerAst/test/ast/ProgramTests.kt b/compilerAst/test/ast/ProgramTests.kt new file mode 100644 index 000000000..9020d6936 --- /dev/null +++ b/compilerAst/test/ast/ProgramTests.kt @@ -0,0 +1,118 @@ +package prog8tests + +import prog8tests.helpers.* +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.* + + +import prog8.ast.Program +import prog8.ast.Module +import prog8.ast.base.Position +import prog8.ast.internedStringsModuleName +import java.lang.IllegalArgumentException +import kotlin.test.assertContains +import kotlin.test.assertNotSame +import kotlin.test.assertSame + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ProgramTests { + + @Nested + inner class Constructor { + @Test + fun withNameBuiltinsAndMemsizer() { + val program = Program("foo", DummyFunctions, DummyMemsizer) + assertThat(program.modules.size, equalTo(1)) + assertThat(program.modules[0].name, equalTo(internedStringsModuleName)) + assertSame(program, program.modules[0].program) + assertSame(program.namespace, program.modules[0].parent) + } + + } + + @Nested + inner class AddModule { + @Test + fun withEmptyModule() { + val program = Program("foo", DummyFunctions, DummyMemsizer) + val m1 = Module("bar", mutableListOf(), Position.DUMMY, null) + + val retVal = program.addModule(m1) + + assertSame(program, retVal) + assertThat(program.modules.size, equalTo(2)) + assertContains(program.modules, m1) + assertSame(program, m1.program) + assertSame(program.namespace, m1.parent) + + assertThrows { program.addModule(m1) } + .let { assertThat(it.message, containsString(m1.name)) } + + val m2 = Module(m1.name, mutableListOf(), m1.position, m1.source) + assertThrows { program.addModule(m2) } + .let { assertThat(it.message, containsString(m2.name)) } + } + } + + @Nested + inner class MoveModuleToFront { + @Test + fun withInternedStringsModule() { + val program = Program("foo", DummyFunctions, DummyMemsizer) + val m = program.modules[0] + assertThat(m.name, equalTo(internedStringsModuleName)) + + val retVal = program.moveModuleToFront(m) + assertSame(program, retVal) + assertSame(m, program.modules[0]) + } + @Test + fun withForeignModule() { + val program = Program("foo", DummyFunctions, DummyMemsizer) + val m = Module("bar", mutableListOf(), Position.DUMMY, null) + + assertThrows { program.moveModuleToFront(m) } + } + @Test + fun withFirstOfPreviouslyAddedModules() { + val program = Program("foo", DummyFunctions, DummyMemsizer) + val m1 = Module("bar", mutableListOf(), Position.DUMMY, null) + val m2 = Module("qmbl", mutableListOf(), Position.DUMMY, null) + program.addModule(m1) + program.addModule(m2) + + val retVal = program.moveModuleToFront(m1) + assertSame(program, retVal) + assertThat(program.modules.indexOf(m1), equalTo(0)) + } + @Test + fun withSecondOfPreviouslyAddedModules() { + val program = Program("foo", DummyFunctions, DummyMemsizer) + val m1 = Module("bar", mutableListOf(), Position.DUMMY, null) + val m2 = Module("qmbl", mutableListOf(), Position.DUMMY, null) + program.addModule(m1) + program.addModule(m2) + + val retVal = program.moveModuleToFront(m2) + assertSame(program, retVal) + assertThat(program.modules.indexOf(m2), equalTo(0)) + } + } + + @Nested + inner class Properties { + @Test + fun modules() { + val program = Program("foo", DummyFunctions, DummyMemsizer) + + val ms1 = program.modules + val ms2 = program.modules + assertSame(ms1, ms2) + } + } +} diff --git a/compilerAst/test/fixtures/empty.p8 b/compilerAst/test/fixtures/empty.p8 new file mode 100644 index 000000000..e69de29bb diff --git a/compilerAst/test/fixtures/file_with_syntax_error.p8 b/compilerAst/test/fixtures/file_with_syntax_error.p8 new file mode 100644 index 000000000..6b9930fe0 --- /dev/null +++ b/compilerAst/test/fixtures/file_with_syntax_error.p8 @@ -0,0 +1,2 @@ +; test expects the following 2nd (!) line: +bad { } ; -> missing EOL at '}' (ie: *after* the '{') diff --git a/compilerAst/test/fixtures/simple_main.p8 b/compilerAst/test/fixtures/simple_main.p8 new file mode 100644 index 000000000..fb81add50 --- /dev/null +++ b/compilerAst/test/fixtures/simple_main.p8 @@ -0,0 +1,4 @@ +main { + sub start() { + } +} diff --git a/compilerAst/test/helpers/DummyFunctions.kt b/compilerAst/test/helpers/DummyFunctions.kt new file mode 100644 index 000000000..e212ea9ea --- /dev/null +++ b/compilerAst/test/helpers/DummyFunctions.kt @@ -0,0 +1,21 @@ +package prog8tests.helpers + +import prog8.ast.IBuiltinFunctions +import prog8.ast.IMemSizer +import prog8.ast.base.Position +import prog8.ast.expressions.Expression +import prog8.ast.expressions.InferredTypes +import prog8.ast.expressions.NumericLiteralValue + +val DummyFunctions = object : IBuiltinFunctions { + override val names: Set = emptySet() + override val purefunctionNames: Set = emptySet() + override fun constValue( + name: String, + args: List, + position: Position, + memsizer: IMemSizer + ): NumericLiteralValue? = null + + override fun returnType(name: String, args: MutableList) = InferredTypes.InferredType.unknown() +} \ No newline at end of file diff --git a/compilerAst/test/helpers/DummyMemsizer.kt b/compilerAst/test/helpers/DummyMemsizer.kt new file mode 100644 index 000000000..3d226437d --- /dev/null +++ b/compilerAst/test/helpers/DummyMemsizer.kt @@ -0,0 +1,8 @@ +package prog8tests.helpers + +import prog8.ast.IMemSizer +import prog8.ast.base.DataType + +val DummyMemsizer = object : IMemSizer { + override fun memorySize(dt: DataType): Int = 0 +} \ No newline at end of file diff --git a/compilerAst/test/helpers/mapCombinations.kt b/compilerAst/test/helpers/mapCombinations.kt new file mode 100644 index 000000000..e96734cb1 --- /dev/null +++ b/compilerAst/test/helpers/mapCombinations.kt @@ -0,0 +1,25 @@ +package prog8tests.helpers + +fun mapCombinations(dim1: Iterable, dim2: Iterable, combine2: (A, B) -> R) = + sequence { + for (a in dim1) + for (b in dim2) + yield(combine2(a, b)) + }.toList() + +fun mapCombinations(dim1: Iterable, dim2: Iterable, dim3: Iterable, combine3: (A, B, C) -> R) = + sequence { + for (a in dim1) + for (b in dim2) + for (c in dim3) + yield(combine3(a, b, c)) + }.toList() + +fun mapCombinations(dim1: Iterable, dim2: Iterable, dim3: Iterable, dim4: Iterable, combine4: (A, B, C, D) -> R) = + sequence { + for (a in dim1) + for (b in dim2) + for (c in dim3) + for (d in dim4) + yield(combine4(a, b, c, d)) + }.toList() \ No newline at end of file diff --git a/compilerAst/test/helpers/paths.kt b/compilerAst/test/helpers/paths.kt new file mode 100644 index 000000000..4e99518b0 --- /dev/null +++ b/compilerAst/test/helpers/paths.kt @@ -0,0 +1,53 @@ +package prog8tests.helpers + +import kotlin.test.* +import kotlin.io.path.* + +import java.nio.file.Path + + +val workingDir = assumeDirectory("").absolute() // Note: "." does NOT work..! +val fixturesDir = assumeDirectory(workingDir,"test/fixtures") +val resourcesDir = assumeDirectory(workingDir,"res") +val outputDir = assumeDirectory(workingDir, "build/tmp/test") + +fun assumeNotExists(path: Path): Path { + assertFalse(path.exists(), "sanity check: should not exist: ${path.absolute()}") + return path +} + +fun assumeNotExists(pathStr: String): Path = assumeNotExists(Path(pathStr)) +fun assumeNotExists(path: Path, other: String): Path = assumeNotExists(path.div(other)) + +fun assumeReadable(path: Path): Path { + assertTrue(path.isReadable(), "sanity check: should be readable: ${path.absolute()}") + return path +} + +fun assumeReadableFile(path: Path): Path { + assertTrue(path.isRegularFile(), "sanity check: should be normal file: ${path.absolute()}") + return assumeReadable(path) +} + +fun assumeReadableFile(pathStr: String): Path = assumeReadableFile(Path(pathStr)) +fun assumeReadableFile(pathStr: String, other: Path): Path = assumeReadableFile(Path(pathStr), other) +fun assumeReadableFile(pathStr: String, other: String): Path = assumeReadableFile(Path(pathStr), other) +fun assumeReadableFile(path: Path, other: String): Path = assumeReadableFile(path.div(other)) +fun assumeReadableFile(path: Path, other: Path): Path = assumeReadableFile(path.div(other)) + +fun assumeDirectory(path: Path): Path { + assertTrue(path.isDirectory(), "sanity check; should be directory: $path") + return path +} + +fun assumeDirectory(pathStr: String): Path = assumeDirectory(Path(pathStr)) +fun assumeDirectory(path: Path, other: String): Path = assumeDirectory(path.div(other)) +fun assumeDirectory(pathStr: String, other: String): Path = assumeDirectory(Path(pathStr).div(other)) +fun assumeDirectory(pathStr: String, other: Path): Path = assumeDirectory(Path(pathStr).div(other)) + + +@Deprecated("Directories are checked automatically at init.", + ReplaceWith("/* nothing */")) +@Suppress("UNUSED_PARAMETER") +fun sanityCheckDirectories(workingDirName: String? = null) { +} \ No newline at end of file diff --git a/compilerAst/test/helpers_pathsTests.kt b/compilerAst/test/helpers_pathsTests.kt new file mode 100644 index 000000000..5d7222283 --- /dev/null +++ b/compilerAst/test/helpers_pathsTests.kt @@ -0,0 +1,363 @@ +package prog8tests + +import prog8tests.helpers.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows + +import kotlin.io.path.* + + +// Do not move into folder helpers/! +// This folder is also used by compiler/test +// but the testing of the helpers themselves must be performed ONLY HERE. +// +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PathsHelpersTests { + + @Nested + inner class AssumeNotExists { + + @Nested + inner class WithOnePathArg { + + @Test + fun `on non-existing path`() { + val path = fixturesDir.div("i_do_not_exist") + assertThat("should return the path", + assumeNotExists(path), `is`(path)) + } + + @Test + fun `on existing file`() { + assertThrows { + assumeNotExists(fixturesDir.div("simple_main.p8")) + } + } + + @Test + fun `on existing directory`() { + assertThrows { + assumeNotExists(fixturesDir) + } + } + } + + @Nested + inner class WithOneStringArg { + + @Test + fun `on non-existing path`() { + val path = fixturesDir.div("i_do_not_exist") + assertThat("should return the path", + assumeNotExists("$path"), `is`(path)) + } + + @Test + fun `on existing file`() { + val path = fixturesDir.div("simple_main.p8") + assertThrows { + assumeNotExists("$path") + } + } + + @Test + fun `on existing directory`() { + assertThrows { + assumeNotExists("$fixturesDir") + } + } + } + + @Nested + inner class WithPathAndStringArgs { + + @Test + fun `on non-existing path`() { + val path = fixturesDir.div("i_do_not_exist") + assertThat("should return the path", + assumeNotExists(fixturesDir, "i_do_not_exist"), `is`(path)) + } + + @Test + fun `on existing file`() { + assertThrows { + assumeNotExists(fixturesDir, "simple_main.p8") + } + } + + @Test + fun `on existing directory`() { + assertThrows { + assumeNotExists(fixturesDir, "..") + } + } + } + } + + @Nested + inner class AssumeDirectory { + + @Nested + inner class WithOnePathArg { + @Test + fun `on non-existing path`() { + val path = fixturesDir.div("i_do_not_exist") + assertThrows { + assumeDirectory(path) + } + } + + @Test + fun `on existing file`() { + val path = fixturesDir.div("simple_main.p8") + assertThrows { + assumeDirectory(path) + } + } + + @Test + fun `on existing directory`() { + val path = workingDir + assertThat("should return the path", assumeDirectory(path), `is`(path)) + } + } + + @Nested + inner class WithOneStringArg { + @Test + fun `on non-existing path`() { + val path = fixturesDir.div("i_do_not_exist") + assertThrows { + assumeDirectory("$path") + } + } + + @Test + fun `on existing file`() { + val path = fixturesDir.div("simple_main.p8") + assertThrows { + assumeDirectory("$path") + } + } + + @Test + fun `on existing directory`() { + val path = workingDir + assertThat("should return the path", + assumeDirectory("$path"), `is`(path)) + } + } + + @Nested + inner class WithPathAndStringArgs { + @Test + fun `on non-existing path`() { + assertThrows { + assumeDirectory(fixturesDir, "i_do_not_exist") + } + } + + @Test + fun `on existing file`() { + assertThrows { + assumeDirectory(fixturesDir, "simple_main.p8") + } + } + + @Test + fun `on existing directory`() { + val path = workingDir.div("..") + assertThat( + "should return resulting path", + assumeDirectory(workingDir, ".."), `is`(path) + ) + } + } + + @Nested + inner class WithStringAndStringArgs { + @Test + fun `on non-existing path`() { + assertThrows { + assumeDirectory("$fixturesDir", "i_do_not_exist") + } + } + + @Test + fun `on existing file`() { + assertThrows { + assumeDirectory("$fixturesDir", "simple_main.p8") + } + } + + @Test + fun `on existing directory`() { + val path = workingDir.div("..") + assertThat( + "should return resulting path", + assumeDirectory("$workingDir", ".."), `is`(path) + ) + } + } + + @Nested + inner class WithStringAndPathArgs { + @Test + fun `on non-existing path`() { + assertThrows { + assumeDirectory("$fixturesDir", Path("i_do_not_exist")) + } + } + + @Test + fun `on existing file`() { + assertThrows { + assumeDirectory("$fixturesDir", Path("simple_main.p8")) + } + } + + @Test + fun `on existing directory`() { + val path = workingDir.div("..") + assertThat( + "should return resulting path", + assumeDirectory("$workingDir", Path("..")), `is`(path) + ) + } + } + } + + @Nested + inner class AssumeReadableFile { + + @Nested + inner class WithOnePathArg { + + @Test + fun `on non-existing path`() { + val path = fixturesDir.div("i_do_not_exist") + assertThrows { + assumeReadableFile(path) + } + } + + @Test + fun `on readable file`() { + val path = fixturesDir.div("simple_main.p8") + assertThat("should return the path", + assumeReadableFile(path), `is`(path)) + } + + @Test + fun `on directory`() { + assertThrows { + assumeReadableFile(fixturesDir) + } + } + } + + @Nested + inner class WithOneStringArg { + + @Test + fun `on non-existing path`() { + val path = fixturesDir.div("i_do_not_exist") + assertThrows { + assumeReadableFile("$path") + } + } + + @Test + fun `on readable file`() { + val path = fixturesDir.div("simple_main.p8") + assertThat("should return the resulting path", + assumeReadableFile("$path"), `is`(path)) + } + + @Test + fun `on directory`() { + assertThrows { + assumeReadableFile("$fixturesDir") + } + } + } + + @Nested + inner class WithPathAndStringArgs { + @Test + fun `on non-existing path`() { + assertThrows { + assumeReadableFile(fixturesDir, "i_do_not_exist") + } + } + + @Test + fun `on readable file`() { + val path = fixturesDir.div("simple_main.p8") + assertThat("should return the resulting path", + assumeReadableFile(fixturesDir, "simple_main.p8"), `is`(path)) + } + + @Test + fun `on directory`() { + assertThrows { + assumeReadableFile(fixturesDir, "..") + } + } + } + + @Nested + inner class WithPathAndPathArgs { + @Test + fun `on non-existing path`() { + assertThrows { + assumeReadableFile(fixturesDir, Path("i_do_not_exist")) + } + } + + @Test fun `on readable file`() { + assertThat("should return the resulting path", + assumeReadableFile(fixturesDir, Path("simple_main.p8")), + `is`(fixturesDir.div("simple_main.p8")) + ) + } + + @Test + fun `on directory`() { + assertThrows { + assumeReadableFile(fixturesDir, Path("..")) + } + } + } + + @Nested + inner class WithStringAndStringArgs { + @Test + fun `on non-existing path`() { + assertThrows { + assumeReadableFile("$fixturesDir", "i_do_not_exist") + } + } + + @Test + fun `on readable file`() { + assertThat("should return the resulting path", + assumeReadableFile(fixturesDir.toString(), "simple_main.p8"), + `is`(fixturesDir.div("simple_main.p8")) + ) + } + + @Test + fun `on directory`() { + assertThrows { + assumeReadableFile("$fixturesDir", "..") + } + } + } + } +}