Merge pull request #53 from meisl/testability_steps_1_2_3_again

Implement plan for testability
This commit is contained in:
Irmen de Jong 2021-10-10 22:30:29 +02:00 committed by GitHub
commit 0509de76d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 3588 additions and 983 deletions

View File

@ -70,7 +70,8 @@ sourceSets {
}
test {
java {
srcDirs = ["${project.projectDir}/test"]
srcDir "${project.projectDir}/test"
srcDir "${project(':compilerAst').projectDir}/test/helpers"
}
}
}

View File

@ -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<String, FSignature>): IBuilt
builtinFunctionReturnType(name, args, program)
}
private fun parseImports(filepath: Path,
fun parseImports(filepath: Path,
errors: IErrorReporter,
compTarget: ICompilationTarget,
libdirs: List<String>): Triple<Program, CompilationOptions, List<Path>> {
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")
}

View File

@ -0,0 +1,6 @@
package prog8.compiler
interface IStringEncoding {
fun encodeString(str: String, altEncoding: Boolean): List<Short>
fun decodeString(bytes: List<Short>, altEncoding: Boolean): String
}

View File

@ -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<String>) {
private val libpaths: List<Path> = 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("<<<implicit-import>>>", 0, 0, 0))
), Position("<<<implicit-import>>>", 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<Directive>()
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
}
}

View File

@ -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)

View File

@ -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<IAstModification> {
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<String, MutableList<prog8.ast.Module>>().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)

View File

@ -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<Short>
override fun decodeString(bytes: List<Short>, 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

View File

@ -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}"

View File

@ -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 {

View File

@ -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<IAstModification> {
// @( &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.

View File

@ -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<Expression>()
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<Expression>()
val refValue = ArrayLiteralValue(InferredTypes.InferredType.known(DataType.ARRAY_F), array, position = numericLv.position)
return listOf(IAstModification.ReplaceNode(decl.value!!, refValue, decl))
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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<String> = emptySet()
override val purefunctionNames: Set<String> = emptySet()
override fun constValue(name: String, args: List<Expression>, position: Position, memsizer: IMemSizer): NumericLiteralValue? = null
override fun returnType(name: String, args: MutableList<Expression>) = 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
}

View File

@ -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<String>) =
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<NoSuchFileException> { 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<NoSuchFileException> { 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<AccessDeniedException> { 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<AccessDeniedException> { 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<ParseError>(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<ParseError>(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<NoSuchFileException>(count[n] + " call / NO .p8 extension")
{ importer.importLibraryModule(filenameNoExt) }.let {
assertThat(it.message!!, containsString(filenameWithExt))
}
assertThat(program.modules.size, equalTo(1))
assertThrows<NoSuchFileException>(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<ParseError>(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<ParseError>(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)
}
}
}
}
}

View File

@ -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<IFunctionCall>()[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<IFunctionCall>()[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<IdentifierReference>(funCall.args[0]) // make test fail
}
}
}

View File

@ -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
)
}

View File

@ -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<FunctionCallStatement>()
.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<FunctionCallStatement>()
.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<FunctionCallStatement>()
.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<DynamicTest> =
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"
)
}
}
}
}

View File

@ -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<VarDecl>()[0]
val rhsValues = (decl.value as ArrayLiteralValue)
.value // Array<Expression>
.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<VarDecl>()[0]
val rhsValues = (decl.value as ArrayLiteralValue)
.value // Array<Expression>
.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<VarDecl>()
.first { it.name == varName }
}
inline fun <reified T : Expression> VarDecl.rhs() : T {
return value as T
}
inline fun <reified T : Expression> ArrayLiteralValue.elements() : List<T> {
return value.map { it as T }
}
fun <N : Number> assertEndpoints(expFirst: N, expLast: N, actual: Iterable<N>, 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<ForLoop>()
.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<ForLoop>()
.map { it.iterable }
.filterIsInstance<RangeExpr>()[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<ForLoop>()
.map { it.iterable }
.filterIsInstance<RangeExpr>()[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<ForLoop>()
.map { it.iterable }
.filterIsInstance<IdentifierReference>()[0]
assertEquals(DataType.STR, iterable.inferType(program).typeOrElse(DataType.UNDEFINED))
}
}

View File

@ -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<String>) =
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()
}
}

View File

@ -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)
}
}

View File

@ -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<String> = emptySet()
override val purefunctionNames: Set<String> = emptySet()
override fun constValue(name: String, args: List<Expression>, position: Position, memsizer: IMemSizer): NumericLiteralValue? = null
override fun returnType(name: String, args: MutableList<Expression>) = 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))
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,10 @@
main {
sub start() {
stuff.do_nothing()
}
}
stuff $1000 {
romsub $1000 = do_nothing()
%asmbinary "do_nothing1.bin", 0
}

View File

@ -0,0 +1,10 @@
main {
sub start() {
stuff.do_nothing()
}
}
stuff $1000 {
romsub $1000 = do_nothing()
%asmbinary "subFolder/do_nothing2.bin", 0
}

View File

@ -0,0 +1,10 @@
main {
sub start() {
stuff.do_nothing()
}
}
stuff $1000 {
romsub $1000 = do_nothing()
%asmbinary "i_do_not_exist.bin", 0
}

View File

@ -0,0 +1,10 @@
main {
sub start() {
stuff.do_nothing()
}
}
stuff $1000 {
romsub $1000 = do_nothing()
%asmbinary "subFolder", 0
}

View File

@ -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"
}
}

View File

@ -1,7 +0,0 @@
main {
romsub $FFD2 = chrout(ubyte ch @ A)
sub start() {
const ubyte ch = '\n'
chrout(ch)
}
}

View File

@ -1,6 +0,0 @@
main {
romsub $FFD2 = chrout(ubyte ch @ A)
sub start() {
chrout('\n')
}
}

View File

@ -1,7 +0,0 @@
main {
romsub $FFD2 = chrout(ubyte ch @ A)
sub start() {
ubyte ch = '\n'
chrout(ch)
}
}

7
compiler/test/fixtures/do_nothing.asm vendored Normal file
View File

@ -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

View File

@ -0,0 +1 @@
`

View File

@ -0,0 +1,2 @@
; test expects the following 2nd (!) line:
bad { } ; -> missing EOL at '}' (ie: *after* the '{')

2
compiler/test/fixtures/foo_bar.asm vendored Normal file
View File

@ -0,0 +1,2 @@
bar .text "foo.bar",0

3
compiler/test/fixtures/foo_bar.p8 vendored Normal file
View File

@ -0,0 +1,3 @@
foo {
str bar = "foo.bar"
}

View File

@ -0,0 +1,9 @@
%import textio
%import foo_bar
main {
str myBar = "main.bar"
sub start() {
txt.print(myBar)
txt.print(foo.bar)
}
}

View File

@ -0,0 +1,9 @@
%import textio
%import "foo_bar.p8"
main {
str myBar = "main.bar"
sub start() {
txt.print(myBar)
txt.print(foo.bar)
}
}

View File

@ -0,0 +1 @@
%import file_with_syntax_error

View File

@ -0,0 +1 @@
%import import_nonexisting

View File

@ -0,0 +1 @@
%import i_do_not_exist

4
compiler/test/fixtures/simple_main.p8 vendored Normal file
View File

@ -0,0 +1,4 @@
main {
sub start() {
}
}

View File

@ -0,0 +1 @@
`

View File

@ -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)
}

View File

@ -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"]
}
}
}

View File

@ -3,6 +3,7 @@
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
</content>

View File

@ -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

View File

@ -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"
}

View File

@ -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)}\"")
}

View File

@ -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<Short>
fun decodeString(bytes: List<Short>, 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<Module>(this)!!
}
fun definingSubroutine(): Subroutine? = findParentNode<Subroutine>(this)
fun definingScope(): INameScope {
val scope = findParentNode<INameScope>(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<Block>(this)!!
}
fun containingStatement(): Statement {
if(this is Statement)
return this
return findParentNode<Statement>(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<Expression>
}
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<Module>(this)!!
}
fun definingSubroutine(): Subroutine? = findParentNode<Subroutine>(this)
fun definingScope(): INameScope {
val scope = findParentNode<INameScope>(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<Block>(this)!!
}
fun containingStatement(): Statement {
if(this is Statement)
return this
return findParentNode<Statement>(this)!!
}
fun replaceChildNode(node: Node, replacement: Node)
}
interface IMemSizer {
fun memorySize(dt: DataType): Int
}
interface IBuiltinFunctions {
val names: Set<String>
val purefunctionNames: Set<String>
fun constValue(name: String, args: List<Expression>, position: Position, memsizer: IMemSizer): NumericLiteralValue?
fun returnType(name: String, args: MutableList<Expression>): InferredTypes.InferredType
}
/*********** Everything starts from here, the Program; zero or more modules *************/
class Program(val name: String,
val modules: MutableList<Module>,
val builtinFunctions: IBuiltinFunctions,
val memsizer: IMemSizer): Node {
val namespace = GlobalNamespace(modules, builtinFunctions.names)
private val _modules = mutableListOf<Module>()
val mainModule: Module
val modules: List<Module> = _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<Block> = modules.flatMap { it.statements.filterIsInstance<Block>() }
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<Pair<String, Boolean>, List<String>>()
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<String> {
// 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<Block> = modules.flatMap { it.statements.filterIsInstance<Block>() }
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<Statement>,
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<Module>, private val builtinFunctionNames: Set<String>): Node, INameScope {
class GlobalNamespace(val modules: Iterable<Module>, private val builtinFunctionNames: Set<String>): Node, INameScope {
override val name = "<<<global>>>"
override val position = Position("<<<global>>>", 0, 0, 0)
override val statements = mutableListOf<Statement>() // 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")
}
}

View File

@ -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")
}
}

View File

@ -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<String>
val purefunctionNames: Set<String>
fun constValue(name: String, args: List<Expression>, position: Position, memsizer: IMemSizer): NumericLiteralValue?
fun returnType(name: String, args: MutableList<Expression>): InferredTypes.InferredType
}

View File

@ -0,0 +1,7 @@
package prog8.ast
import prog8.ast.base.DataType
interface IMemSizer {
fun memorySize(dt: DataType): Int
}

View File

@ -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> =
statement().asSequence().map { it.toAst(encoding) }.toMutableList()
private fun Prog8ANTLRParser.Statement_blockContext.toAst(): MutableList<Statement> =
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<AsmSubroutinePar
AsmSubroutineParameter(vardecl.varname.text, datatype, registerorpair, statusregister, toPosition())
}
private fun Prog8ANTLRParser.Functioncall_stmtContext.toAst(encoding: IStringEncoding): Statement {
private fun Prog8ANTLRParser.Functioncall_stmtContext.toAst(): Statement {
val void = this.VOID() != null
val location = scoped_identifier().toAst()
return if(expression_list() == null)
FunctionCallStatement(location, mutableListOf(), void, toPosition())
else
FunctionCallStatement(location, expression_list().toAst(encoding).toMutableList(), void, toPosition())
FunctionCallStatement(location, expression_list().toAst().toMutableList(), void, toPosition())
}
private fun Prog8ANTLRParser.FunctioncallContext.toAst(encoding: IStringEncoding): FunctionCall {
private fun Prog8ANTLRParser.FunctioncallContext.toAst(): FunctionCall {
val location = scoped_identifier().toAst()
return if(expression_list() == null)
FunctionCall(location, mutableListOf(), toPosition())
else
FunctionCall(location, expression_list().toAst(encoding).toMutableList(), toPosition())
FunctionCall(location, expression_list().toAst().toMutableList(), toPosition())
}
private fun Prog8ANTLRParser.InlineasmContext.toAst() =
InlineAssembly(INLINEASMBLOCK().text, toPosition())
private fun Prog8ANTLRParser.ReturnstmtContext.toAst(encoding: IStringEncoding) : Return {
return Return(expression()?.toAst(encoding), toPosition())
private fun Prog8ANTLRParser.ReturnstmtContext.toAst() : Return {
return Return(expression()?.toAst(), toPosition())
}
private fun Prog8ANTLRParser.UnconditionaljumpContext.toAst(): Jump {
@ -305,14 +296,14 @@ private fun Prog8ANTLRParser.UnconditionaljumpContext.toAst(): Jump {
private fun Prog8ANTLRParser.LabeldefContext.toAst(): Statement =
Label(children[0].text, toPosition())
private fun Prog8ANTLRParser.SubroutineContext.toAst(encoding: IStringEncoding) : Subroutine {
private fun Prog8ANTLRParser.SubroutineContext.toAst() : Subroutine {
// non-asm subroutine
val inline = inline()!=null
val returntypes = sub_return_part()?.toAst() ?: emptyList()
return Subroutine(identifier().text,
sub_params()?.toAst() ?: emptyList(),
returntypes,
statement_block()?.toAst(encoding) ?: mutableListOf(),
statement_block()?.toAst() ?: mutableListOf(),
inline,
toPosition())
}
@ -328,12 +319,12 @@ private fun Prog8ANTLRParser.Sub_paramsContext.toAst(): List<SubroutineParameter
SubroutineParameter(it.varname.text, datatype, it.toPosition())
}
private fun Prog8ANTLRParser.Assign_targetContext.toAst(encoding: IStringEncoding) : AssignTarget {
private fun Prog8ANTLRParser.Assign_targetContext.toAst() : AssignTarget {
val identifier = scoped_identifier()
return when {
identifier!=null -> 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<CpuRegister> {
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> =
expression().map { it.toAst(encoding) }.toTypedArray()
private fun Prog8ANTLRParser.ArrayliteralContext.toAst() : Array<Expression> =
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<Statement> {
return statement_block()?.toAst(encoding) ?: mutableListOf(statement().toAst(encoding))
private fun Prog8ANTLRParser.Else_partContext.toAst(): MutableList<Statement> {
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,

View File

@ -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<String>, override val position: Position) : Expression(), IAssignable {
override lateinit var parent: Node

View File

@ -110,6 +110,7 @@ abstract class AstWalker {
open fun before(untilLoop: UntilLoop, parent: Node): Iterable<IAstModification> = noModifications
open fun before(returnStmt: Return, parent: Node): Iterable<IAstModification> = noModifications
open fun before(scope: AnonymousScope, parent: Node): Iterable<IAstModification> = noModifications
open fun before(char: CharLiteral, parent: Node): Iterable<IAstModification> = noModifications
open fun before(string: StringLiteralValue, parent: Node): Iterable<IAstModification> = noModifications
open fun before(subroutine: Subroutine, parent: Node): Iterable<IAstModification> = noModifications
open fun before(typecast: TypecastExpression, parent: Node): Iterable<IAstModification> = noModifications
@ -150,6 +151,7 @@ abstract class AstWalker {
open fun after(untilLoop: UntilLoop, parent: Node): Iterable<IAstModification> = noModifications
open fun after(returnStmt: Return, parent: Node): Iterable<IAstModification> = noModifications
open fun after(scope: AnonymousScope, parent: Node): Iterable<IAstModification> = noModifications
open fun after(char: CharLiteral, parent: Node): Iterable<IAstModification> = noModifications
open fun after(string: StringLiteralValue, parent: Node): Iterable<IAstModification> = noModifications
open fun after(subroutine: Subroutine, parent: Node): Iterable<IAstModification> = noModifications
open fun after(typecast: TypecastExpression, parent: Node): Iterable<IAstModification> = 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)

View File

@ -79,6 +79,9 @@ interface IAstVisitor {
fun visit(numLiteral: NumericLiteralValue) {
}
fun visit(char: CharLiteral) {
}
fun visit(string: StringLiteralValue) {
}

View File

@ -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<String>) {
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("<<<implicit-import>>>", 0, 0, 0))
), Position("<<<implicit-import>>>", 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<Directive>()
importedModule.statements.removeAll(directives)
directives = directives.filter{ it.directive !in moduleLevelDirectives }
importedModule.statements.addAll(0, directives)
}
private fun tryGetModuleFromResource(name: String, compilationTargetName: String): Pair<InputStream, String>? {
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)")
}
}

View File

@ -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 = "<String@123beef>"
.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
}
}

View File

@ -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])
* * `<String@44c56085>` 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 `<String@44c56085>`.
*/
fun of(text: String): SourceCode {
return object : SourceCode() {
override val isFromResources = false
override val origin = "<String@${System.identityHashCode(text).toString(16)}>"
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]: `<res:/x/y/z.p8>` 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])
*/
}
}

View File

@ -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<Short> {
TODO("Not yet implemented")
}
override fun decodeString(bytes: List<Short>, altEncoding: Boolean): String {
TODO("Not yet implemented")
}
}
object DummyFunctions: IBuiltinFunctions {
override val names: Set<String> = emptySet()
override val purefunctionNames: Set<String> = emptySet()
override fun constValue(name: String, args: List<Expression>, position: Position, memsizer: IMemSizer): NumericLiteralValue? = null
override fun returnType(name: String, args: MutableList<Expression>) = 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 <EOL> at '<EOF>'"
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<ParsingFailedError> { 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<ParsingFailedError>{ parseModule("""
blockA {
} blockB {
}
""") }
// block and directive
assertFailsWith<ParsingFailedError>{ 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<ParsingFailedError>{ parseModule("""
%import textio blockB {
}
""") }
assertFailsWith<ParsingFailedError>{ 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<ParsingFailedError>(){ 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<Block>(ast.statements.first())
assertEquals((ast.statements.first() as Block).name, "main")
}
}

View File

@ -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<String, Module> {
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")
}
}

View File

@ -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 <EOL> at '<EOF>'"
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<ParseError> { 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<ParseError>{ parseModule(SourceCode.of("""
blockA {
} blockB {
}
""")) }
// block and directive
assertFailsWith<ParseError>{ 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<ParseError>{ parseModule(SourceCode.of("""
%import textio blockB {
}
""")) }
assertFailsWith<ParseError>{ 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<ParseError> { parseModule(SourceCode.of(srcText)) }
try {
parseModule(SourceCode.of(srcText))
} catch (e: ParseError) {
assertPosition(e.position, Regex("^<String@[0-9a-f]+>$"), 1, 4, 4)
}
}
@Test
fun `in ParseError from bad file source code`() {
val path = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8")
assertFailsWith<ParseError> { 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("^<String@[0-9a-f]+>$"), 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<Block>()[0]
assertPositionOf(mainBlock, mpf, 1, 0) // TODO: endCol wrong!
val startSub = mainBlock.statements.filterIsInstance<Subroutine>()[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<Directive>()[0]
assertPositionOf(targetDirective, mpf, 1, 0) // TODO: endCol wrong!
val mainBlock = module.statements.filterIsInstance<Block>()[0]
assertPositionOf(mainBlock, mpf, 2, 0) // TODO: endCol wrong!
val startSub = mainBlock.statements.filterIsInstance<Subroutine>()[0]
assertPositionOf(startSub, mpf, 3, 4) // TODO: endCol wrong!
val declFoo = startSub.statements.filterIsInstance<VarDecl>()[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<VarDecl>()[1]
assertPositionOf(declBar, mpf, 5, 8) // TODO: endCol wrong!
val whenStmt = startSub.statements.filterIsInstance<WhenStatement>()[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<Block>()[0]
.statements.filterIsInstance<Subroutine>()[0]
val funCall = startSub.statements.filterIsInstance<IFunctionCall>().first()
assertIs<CharLiteral>(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<Block>()[0]
.statements.filterIsInstance<VarDecl>()[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<Block>()[0]
.statements.filterIsInstance<VarDecl>()[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<Block>()[0]
.statements.filterIsInstance<Subroutine>()[0]
.statements.filterIsInstance<VarDecl>()[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<Block>()[0]
.statements.filterIsInstance<Subroutine>()[0]
.statements.filterIsInstance<VarDecl>()[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<Block>()[0]
.statements.filterIsInstance<Subroutine>()[0]
.statements.filterIsInstance<ForLoop>()
.map { it.iterable }
assertEquals(5, iterables.size)
val it0 = iterables[0] as RangeExpr
assertIs<StringLiteralValue>(it0.from, "parser should leave it as is")
assertIs<StringLiteralValue>(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<CharLiteral>(it2.from, "parser should leave it as is")
assertIs<CharLiteral>(it2.to, "parser should leave it as is")
val it3 = iterables[3] as RangeExpr
// TODO: intro BoolLiteral
assertIs<NumericLiteralValue>(it3.from, "parser should leave it as is")
assertIs<NumericLiteralValue>(it3.to, "parser should leave it as is")
val it4 = iterables[4] as RangeExpr
assertIs<NumericLiteralValue>(it4.from, "parser should leave it as is")
assertIs<NumericLiteralValue>(it4.to, "parser should leave it as is")
}
}
}

View File

@ -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("^<String@[0-9a-f]+>$"))
assertEquals(text, actualText)
}
@Test
fun testFromPathWithNonExistingPath() {
val filename = "i_do_not_exist.p8"
val path = assumeNotExists(fixturesDir, filename)
assertFailsWith<NoSuchFileException> { SourceCode.fromPath(path) }
}
@Test
fun testFromPathWithMissingExtension_p8() {
val pathWithoutExt = assumeNotExists(fixturesDir,"simple_main")
assumeReadableFile(fixturesDir,"simple_main.p8")
assertFailsWith<NoSuchFileException> { SourceCode.fromPath(pathWithoutExt) }
}
@Test
fun testFromPathWithDirectory() {
assertFailsWith<AccessDeniedException> { 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<NoSuchFileException> { SourceCode.fromResources(pathString) }
}
@Test
fun testFromResourcesWithNonExistingFile_withoutLeadingSlash() {
val pathString = "prog8lib/i_do_not_exist"
assumeNotExists(resourcesDir, pathString)
assertThrows<NoSuchFileException> { 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<AccessDeniedException> { SourceCode.fromResources(pathString) }
}
}

View File

@ -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<IllegalArgumentException> { program.addModule(m1) }
.let { assertThat(it.message, containsString(m1.name)) }
val m2 = Module(m1.name, mutableListOf(), m1.position, m1.source)
assertThrows<IllegalArgumentException> { 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<IllegalArgumentException> { 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)
}
}
}

0
compilerAst/test/fixtures/empty.p8 vendored Normal file
View File

View File

@ -0,0 +1,2 @@
; test expects the following 2nd (!) line:
bad { } ; -> missing EOL at '}' (ie: *after* the '{')

View File

@ -0,0 +1,4 @@
main {
sub start() {
}
}

View File

@ -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<String> = emptySet()
override val purefunctionNames: Set<String> = emptySet()
override fun constValue(
name: String,
args: List<Expression>,
position: Position,
memsizer: IMemSizer
): NumericLiteralValue? = null
override fun returnType(name: String, args: MutableList<Expression>) = InferredTypes.InferredType.unknown()
}

View File

@ -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
}

View File

@ -0,0 +1,25 @@
package prog8tests.helpers
fun <A, B, R> mapCombinations(dim1: Iterable<A>, dim2: Iterable<B>, combine2: (A, B) -> R) =
sequence {
for (a in dim1)
for (b in dim2)
yield(combine2(a, b))
}.toList()
fun <A, B, C, R> mapCombinations(dim1: Iterable<A>, dim2: Iterable<B>, dim3: Iterable<C>, 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 <A, B, C, D, R> mapCombinations(dim1: Iterable<A>, dim2: Iterable<B>, dim3: Iterable<C>, dim4: Iterable<D>, 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()

View File

@ -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) {
}

View File

@ -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<java.lang.AssertionError> {
assumeNotExists(fixturesDir.div("simple_main.p8"))
}
}
@Test
fun `on existing directory`() {
assertThrows<java.lang.AssertionError> {
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<java.lang.AssertionError> {
assumeNotExists("$path")
}
}
@Test
fun `on existing directory`() {
assertThrows<java.lang.AssertionError> {
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<java.lang.AssertionError> {
assumeNotExists(fixturesDir, "simple_main.p8")
}
}
@Test
fun `on existing directory`() {
assertThrows<java.lang.AssertionError> {
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<AssertionError> {
assumeDirectory(path)
}
}
@Test
fun `on existing file`() {
val path = fixturesDir.div("simple_main.p8")
assertThrows<AssertionError> {
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<AssertionError> {
assumeDirectory("$path")
}
}
@Test
fun `on existing file`() {
val path = fixturesDir.div("simple_main.p8")
assertThrows<AssertionError> {
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<AssertionError> {
assumeDirectory(fixturesDir, "i_do_not_exist")
}
}
@Test
fun `on existing file`() {
assertThrows<AssertionError> {
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<AssertionError> {
assumeDirectory("$fixturesDir", "i_do_not_exist")
}
}
@Test
fun `on existing file`() {
assertThrows<AssertionError> {
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<AssertionError> {
assumeDirectory("$fixturesDir", Path("i_do_not_exist"))
}
}
@Test
fun `on existing file`() {
assertThrows<AssertionError> {
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<AssertionError> {
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<AssertionError> {
assumeReadableFile(fixturesDir)
}
}
}
@Nested
inner class WithOneStringArg {
@Test
fun `on non-existing path`() {
val path = fixturesDir.div("i_do_not_exist")
assertThrows<AssertionError> {
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<AssertionError> {
assumeReadableFile("$fixturesDir")
}
}
}
@Nested
inner class WithPathAndStringArgs {
@Test
fun `on non-existing path`() {
assertThrows<java.lang.AssertionError> {
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<AssertionError> {
assumeReadableFile(fixturesDir, "..")
}
}
}
@Nested
inner class WithPathAndPathArgs {
@Test
fun `on non-existing path`() {
assertThrows<java.lang.AssertionError> {
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<AssertionError> {
assumeReadableFile(fixturesDir, Path(".."))
}
}
}
@Nested
inner class WithStringAndStringArgs {
@Test
fun `on non-existing path`() {
assertThrows<java.lang.AssertionError> {
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<AssertionError> {
assumeReadableFile("$fixturesDir", "..")
}
}
}
}
}