diff --git a/docs/source/programming.rst b/docs/source/programming.rst index b38434063..c251b7cd7 100644 --- a/docs/source/programming.rst +++ b/docs/source/programming.rst @@ -203,7 +203,9 @@ Integers Integers are 8 or 16 bit numbers and can be written in normal decimal notation, in hexadecimal and in binary notation. -@todo right now only unsinged integers are supported (0-255 for byte types, 0-65535 for word types) +.. todo:: + Right now only unsinged integers are supported (0-255 for byte types, 0-65535 for word types) + @todo maybe signed integers (-128..127 and -32768..32767) will be added later Strings @@ -254,14 +256,9 @@ The resulting value is simply a 16 bit word. Example:: AX = #somevar -**Indirect addressing:** -@todo ??? - -**Indirect addressing in jumps:** -@todo ??? -For an indirect ``goto`` statement, the compiler will issue the 6502 CPU's special instruction -(``jmp`` indirect). A subroutine call (``jsr`` indirect) is emitted -using a couple of instructions. +.. todo:: + This is not yet implemented. + Indirect addressing, Indirect addressing in jumps (jmp/jsr indirect) Loops @@ -321,11 +318,13 @@ for normal assignments (``A = A + X``). Expressions ----------- -In most places where a number or other value is expected, you can use just the number, or a full constant expression. +In most places where a number or other value is expected, you can use just the number, or a constant expression. The expression is parsed and evaluated by the compiler itself at compile time, and the (constant) resulting value is used in its place. -Expressions can contain function calls to the math library (sin, cos, etc) and you can also use -all builtin functions (max, avg, min, sum etc). They can also reference idendifiers defined elsewhere in your code, -if this makes sense. +Expressions can contain procedure and function calls. +There are various built-in functions such as sin(), cos(), min(), max() that can be used in expressions (see :ref:`builtinfunctions`). +You can also reference idendifiers defined elsewhere in your code. +The compiler will evaluate the expression if it is a constant, and just use the resulting value from then on. +Expressions that cannot be compile-time evaluated will result in code that calculates them at runtime. Arithmetic and Logical expressions @@ -378,6 +377,13 @@ value of the given registers after the subroutine call. Otherwise, the subrouti as well clobber all three registers. Preserving the original values does result in some stack manipulation code to be inserted for every call like this, which can be quite slow. +.. caution:: + Note that *recursive* subroutine calls are not supported at this time. + If you do need a recursive algorithm, you'll have to hand code it in embedded assembly for now, + or rewrite it into an iterative algorithm. + + +.. _builtinfunctions: Built-in Functions ------------------ @@ -473,3 +479,6 @@ _P_carry(bit) _P_irqd(bit) Set (or clear) the CPU status register Interrupt Disable flag. No result value. (translated into ``SEI`` or ``CLI`` cpu instruction) + +.. todo:: + additional builtins such as: avg, sum, abs, round diff --git a/il65/examples/test.ill b/il65/examples/test.ill index d8b228949..bc98574d0 100644 --- a/il65/examples/test.ill +++ b/il65/examples/test.ill @@ -33,6 +33,7 @@ const byte equal = 4==4 const byte equal2 = (4+hopla)>0 + goto mega if_eq goto mega if_eq { @@ -57,7 +58,7 @@ byte equalWW = 4==4 const byte equalWW2 = (4+hopla)>0 - if (1==1) goto hopla + if (1==1) goto cool if (1==2) return 44 @@ -101,9 +102,14 @@ cool: byte blerp = 3 A=99 return 33 + ultrafoo() X =33 mega: cool: + sub ultrafoo() -> () { + return 33 + goto main.mega + } } diff --git a/il65/src/il65/Main.kt b/il65/src/il65/Main.kt index ef2e8230f..62ba007d6 100644 --- a/il65/src/il65/Main.kt +++ b/il65/src/il65/Main.kt @@ -30,12 +30,14 @@ fun main(args: Array) { // perform syntax checks and optimizations - moduleAst.checkIdentifiers(globalNamespace) + moduleAst.checkIdentifiers() moduleAst.optimizeExpressions(globalNamespace) - val allScopedSymbolDefinitions = moduleAst.checkIdentifiers(globalNamespace) + moduleAst.checkValid(globalNamespace) // check if tree is valid + val allScopedSymbolDefinitions = moduleAst.checkIdentifiers() moduleAst.optimizeStatements(globalNamespace, allScopedSymbolDefinitions) - val globalNamespaceAfterOptimize = moduleAst.namespace() // it could have changed in the meantime - moduleAst.checkValid(globalNamespaceAfterOptimize) // check if final tree is valid + val globalNamespaceAfterOptimize = moduleAst.namespace() // it could have changed in the meantime + moduleAst.checkValid(globalNamespaceAfterOptimize) // check if final tree is valid + moduleAst.checkRecursion() // check if there are recursive subroutine calls // determine special compiler options diff --git a/il65/src/il65/ast/AST.kt b/il65/src/il65/ast/AST.kt index 094ee7214..fd977f42d 100644 --- a/il65/src/il65/ast/AST.kt +++ b/il65/src/il65/ast/AST.kt @@ -168,12 +168,36 @@ interface Node { var position: Position? // optional for the sake of easy unit testing var parent: Node // will be linked correctly later (late init) fun linkParents(parent: Node) + fun definingScope(): INameScope { + val scope = findParentNode(this) + if(scope!=null) { + return scope + } + if(this is Label && this.name.startsWith("pseudo::")) { + return BuiltinFunctionScopePlaceholder + } + throw FatalAstException("scope missing from $this") + } +} + + +// find the parent node of a specific type or interface +// (useful to figure out in what namespace/block something is defined, etc) +inline fun findParentNode(node: Node): T? { + var candidate = node.parent + while(candidate !is T && candidate !is ParentSentinel) + candidate = candidate.parent + return if(candidate is ParentSentinel) + null + else + candidate as T } interface IStatement : Node { fun process(processor: IAstProcessor) : IStatement fun makeScopedName(name: String): List { + // this is usually cached in a lazy property on the statement object itself val scope = mutableListOf() var statementScope = this.parent while(statementScope !is ParentSentinel && statementScope !is Module) { @@ -191,6 +215,7 @@ interface IStatement : Node { interface IFunctionCall { var target: IdentifierReference var arglist: List + var targetStatement: IStatement } interface INameScope { @@ -204,7 +229,7 @@ interface INameScope { fun subScopes() = statements.filter { it is INameScope } .map { it as INameScope }.associate { it.name to it } - fun definedNames() = statements.filter { it is Label || it is VarDecl } + fun labelsAndVariables() = statements.filter { it is Label || it is VarDecl } .associate {((it as? Label)?.name ?: (it as? VarDecl)?.name) to it } fun lookup(scopedName: List, statement: Node) : IStatement? { @@ -217,19 +242,15 @@ interface INameScope { return null } val foundScope : INameScope = scope!! - return foundScope.definedNames()[scopedName.last()] + return foundScope.labelsAndVariables()[scopedName.last()] ?: foundScope.subScopes()[scopedName.last()] as IStatement? } else { // unqualified name, find the scope the statement is in, look in that first - var statementScope: Node = statement + var statementScope = statement while(true) { - while (statementScope !is INameScope && statementScope !is ParentSentinel) - statementScope = statementScope.parent - if (statementScope is ParentSentinel) - return null - val localScope = statementScope as INameScope - val result = localScope.definedNames()[scopedName[0]] + val localScope = statementScope.definingScope() + val result = localScope.labelsAndVariables()[scopedName[0]] if (result != null) return result val subscope = localScope.subScopes()[scopedName[0]] as IStatement? @@ -244,7 +265,7 @@ interface INameScope { fun debugPrint() { fun printNames(indent: Int, namespace: INameScope) { println(" ".repeat(4*indent) + "${namespace.name} -> ${namespace::class.simpleName} at ${namespace.position}") - namespace.definedNames().forEach { + namespace.labelsAndVariables().forEach { println(" ".repeat(4 * (1 + indent)) + "${it.key} -> ${it.value::class.simpleName} at ${it.value.position}") } namespace.subScopes().forEach { @@ -266,7 +287,7 @@ interface INameScope { * Inserted into the Ast in place of modified nodes (not inserted directly as a parser result) * It can hold zero or more replacement statements that have to be inserted at that point. */ -data class AnonymousStatementList(override var parent: Node, var statements: List) : IStatement { +class AnonymousStatementList(override var parent: Node, var statements: List) : IStatement { override var position: Position? = null override fun linkParents(parent: Node) { @@ -281,15 +302,22 @@ data class AnonymousStatementList(override var parent: Node, var statements: Lis } -object ParentSentinel : Node { +private object ParentSentinel : Node { override var position: Position? = null override var parent: Node = this override fun linkParents(parent: Node) {} } +object BuiltinFunctionScopePlaceholder : INameScope { + override val name = "<>" + override val position: Position? = null + override var statements = mutableListOf() + override fun usedNames(): Set = throw NotImplementedError("not implemented on sub-scopes") + override fun registerUsedName(name: String) = throw NotImplementedError("not implemented on sub-scopes") +} -data class Module(override val name: String, - override var statements: MutableList) : Node, INameScope { +class Module(override val name: String, + override var statements: MutableList) : Node, INameScope { override var position: Position? = null override lateinit var parent: Node @@ -326,10 +354,10 @@ data class Module(override val name: String, val stmt = super.lookup(scopedName, statement) if(stmt!=null) { val targetScopedName = when(stmt) { - is Label -> stmt.makeScopedName(stmt.name) - is VarDecl -> stmt.makeScopedName(stmt.name) - is Block -> stmt.makeScopedName(stmt.name) - is Subroutine -> stmt.makeScopedName(stmt.name) + is Label -> stmt.scopedname + is VarDecl -> stmt.scopedname + is Block -> stmt.scopedname + is Subroutine -> stmt.scopedname else -> throw NameError("wrong identifier target: $stmt", stmt.position) } registerUsedName(targetScopedName.joinToString(".")) @@ -350,11 +378,12 @@ data class Module(override val name: String, } -data class Block(override val name: String, +class Block(override val name: String, val address: Int?, override var statements: MutableList) : IStatement, INameScope { override var position: Position? = null override lateinit var parent: Node + val scopedname: List by lazy { makeScopedName(name) } override fun linkParents(parent: Node) { this.parent = parent @@ -372,7 +401,7 @@ data class Block(override val name: String, } -data class Directive(val directive: String, val args: List) : IStatement { +class Directive(val directive: String, val args: List) : IStatement { override var position: Position? = null override lateinit var parent: Node @@ -385,7 +414,7 @@ data class Directive(val directive: String, val args: List) : ISta } -data class DirectiveArg(val str: String?, val name: String?, val int: Int?) : Node { +class DirectiveArg(val str: String?, val name: String?, val int: Int?) : Node { override var position: Position? = null override lateinit var parent: Node @@ -395,19 +424,24 @@ data class DirectiveArg(val str: String?, val name: String?, val int: Int?) : No } -data class Label(val name: String) : IStatement { +class Label(val name: String) : IStatement { override var position: Position? = null override lateinit var parent: Node + val scopedname: List by lazy { makeScopedName(name) } override fun linkParents(parent: Node) { this.parent = parent } override fun process(processor: IAstProcessor) = processor.process(this) + + override fun toString(): String { + return "Label(name=$name, pos=$position)" + } } -data class Return(var values: List) : IStatement { +class Return(var values: List) : IStatement { override var position: Position? = null override lateinit var parent: Node @@ -423,7 +457,7 @@ data class Return(var values: List) : IStatement { } -data class ArraySpec(var x: IExpression, var y: IExpression?) : Node { +class ArraySpec(var x: IExpression, var y: IExpression?) : Node { override var position: Position? = null override lateinit var parent: Node @@ -446,7 +480,7 @@ enum class VarDeclType { MEMORY } -data class VarDecl(val type: VarDeclType, +class VarDecl(val type: VarDeclType, val datatype: DataType, val arrayspec: ArraySpec?, val name: String, @@ -465,6 +499,8 @@ data class VarDecl(val type: VarDeclType, val isScalar = arrayspec==null val isArray = arrayspec!=null && arrayspec.y==null val isMatrix = arrayspec?.y != null + val scopedname: List by lazy { makeScopedName(name) } + fun arraySizeX(namespace: INameScope) : Int? { return arrayspec?.x?.constValue(namespace)?.intvalue } @@ -474,7 +510,7 @@ data class VarDecl(val type: VarDeclType, } -data class Assignment(var target: AssignTarget, val aug_op : String?, var value: IExpression) : IStatement { +class Assignment(var target: AssignTarget, val aug_op : String?, var value: IExpression) : IStatement { override var position: Position? = null override lateinit var parent: Node @@ -491,7 +527,7 @@ data class Assignment(var target: AssignTarget, val aug_op : String?, var value: } } -data class AssignTarget(val register: Register?, val identifier: IdentifierReference?) : Node { +class AssignTarget(val register: Register?, val identifier: IdentifierReference?) : Node { override var position: Position? = null override lateinit var parent: Node @@ -513,7 +549,7 @@ interface IExpression: Node { // note: some expression elements are mutable, to be able to rewrite/process the expression tree -data class PrefixExpression(val operator: String, var expression: IExpression) : IExpression { +class PrefixExpression(val operator: String, var expression: IExpression) : IExpression { override var position: Position? = null override lateinit var parent: Node @@ -528,7 +564,7 @@ data class PrefixExpression(val operator: String, var expression: IExpression) : } -data class BinaryExpression(var left: IExpression, val operator: String, var right: IExpression) : IExpression { +class BinaryExpression(var left: IExpression, val operator: String, var right: IExpression) : IExpression { override var position: Position? = null override lateinit var parent: Node @@ -546,7 +582,7 @@ data class BinaryExpression(var left: IExpression, val operator: String, var rig override fun referencesIdentifier(name: String) = left.referencesIdentifier(name) || right.referencesIdentifier(name) } -data class LiteralValue(val intvalue: Int? = null, +class LiteralValue(val intvalue: Int? = null, val floatvalue: Double? = null, val strvalue: String? = null, val arrayvalue: List? = null) : IExpression { @@ -594,7 +630,7 @@ data class LiteralValue(val intvalue: Int? = null, } -data class RangeExpr(var from: IExpression, var to: IExpression) : IExpression { +class RangeExpr(var from: IExpression, var to: IExpression) : IExpression { override var position: Position? = null override lateinit var parent: Node @@ -610,7 +646,7 @@ data class RangeExpr(var from: IExpression, var to: IExpression) : IExpression { } -data class RegisterExpr(val register: Register) : IExpression { +class RegisterExpr(val register: Register) : IExpression { override var position: Position? = null override lateinit var parent: Node @@ -624,7 +660,7 @@ data class RegisterExpr(val register: Register) : IExpression { } -data class IdentifierReference(val nameInSource: List) : IExpression { +class IdentifierReference(val nameInSource: List) : IExpression { override var position: Position? = null override lateinit var parent: Node @@ -645,12 +681,16 @@ data class IdentifierReference(val nameInSource: List) : IExpression { return vardecl.value?.constValue(namespace) } + override fun toString(): String { + return "IdentifierRef($nameInSource)" + } + override fun process(processor: IAstProcessor) = processor.process(this) override fun referencesIdentifier(name: String): Boolean = nameInSource.last() == name // @todo is this correct all the time? } -data class PostIncrDecr(var target: AssignTarget, val operator: String) : IStatement { +class PostIncrDecr(var target: AssignTarget, val operator: String) : IStatement { override var position: Position? = null override lateinit var parent: Node @@ -666,9 +706,10 @@ data class PostIncrDecr(var target: AssignTarget, val operator: String) : IState } -data class Jump(val address: Int?, val identifier: IdentifierReference?) : IStatement { +class Jump(val address: Int?, val identifier: IdentifierReference?) : IStatement { override var position: Position? = null override lateinit var parent: Node + var targetStatement: IStatement? = null override fun linkParents(parent: Node) { this.parent = parent @@ -679,9 +720,10 @@ data class Jump(val address: Int?, val identifier: IdentifierReference?) : IStat } -data class FunctionCall(override var target: IdentifierReference, override var arglist: List) : IExpression, IFunctionCall { +class FunctionCall(override var target: IdentifierReference, override var arglist: List) : IExpression, IFunctionCall { override var position: Position? = null override lateinit var parent: Node + override lateinit var targetStatement: IStatement override fun linkParents(parent: Node) { this.parent = parent @@ -726,14 +768,19 @@ data class FunctionCall(override var target: IdentifierReference, override var a } } + override fun toString(): String { + return "FunctionCall(target=$target, targetStmt=$targetStatement, pos=$position)" + } + override fun process(processor: IAstProcessor) = processor.process(this) override fun referencesIdentifier(name: String): Boolean = target.referencesIdentifier(name) || arglist.any{it.referencesIdentifier(name)} } -data class FunctionCallStatement(override var target: IdentifierReference, override var arglist: List) : IStatement, IFunctionCall { +class FunctionCallStatement(override var target: IdentifierReference, override var arglist: List) : IStatement, IFunctionCall { override var position: Position? = null override lateinit var parent: Node + override lateinit var targetStatement: IStatement override fun linkParents(parent: Node) { this.parent = parent @@ -742,10 +789,14 @@ data class FunctionCallStatement(override var target: IdentifierReference, overr } override fun process(processor: IAstProcessor) = processor.process(this) + + override fun toString(): String { + return "FunctionCall(target=$target, targetStmt=$targetStatement, pos=$position)" + } } -data class InlineAssembly(val assembly: String) : IStatement { +class InlineAssembly(val assembly: String) : IStatement { override var position: Position? = null override lateinit var parent: Node @@ -757,13 +808,14 @@ data class InlineAssembly(val assembly: String) : IStatement { } -data class Subroutine(override val name: String, +class Subroutine(override val name: String, val parameters: List, val returnvalues: List, val address: Int?, override var statements: MutableList) : IStatement, INameScope { override var position: Position? = null override lateinit var parent: Node + val scopedname: List by lazy { makeScopedName(name) } override fun linkParents(parent: Node) { this.parent = parent @@ -783,7 +835,7 @@ data class Subroutine(override val name: String, } -data class SubroutineParameter(val name: String, val register: Register?, val statusflag: Statusflag?) : Node { +class SubroutineParameter(val name: String, val register: Register?, val statusflag: Statusflag?) : Node { override var position: Position? = null override lateinit var parent: Node @@ -793,7 +845,7 @@ data class SubroutineParameter(val name: String, val register: Register?, val st } -data class SubroutineReturnvalue(val register: Register?, val statusflag: Statusflag?, val clobbered: Boolean) : Node { +class SubroutineReturnvalue(val register: Register?, val statusflag: Statusflag?, val clobbered: Boolean) : Node { override var position: Position? = null override lateinit var parent: Node @@ -803,7 +855,7 @@ data class SubroutineReturnvalue(val register: Register?, val statusflag: Status } -data class IfStatement(var condition: IExpression, +class IfStatement(var condition: IExpression, var statements: List, var elsepart: List) : IStatement { override var position: Position? = null @@ -820,7 +872,7 @@ data class IfStatement(var condition: IExpression, } -data class BranchStatement(var condition: BranchCondition, +class BranchStatement(var condition: BranchCondition, var statements: List, var elsepart: List) : IStatement { override var position: Position? = null diff --git a/il65/src/il65/ast/AstChecker.kt b/il65/src/il65/ast/AstChecker.kt index 8d110337f..ed5d2177a 100644 --- a/il65/src/il65/ast/AstChecker.kt +++ b/il65/src/il65/ast/AstChecker.kt @@ -22,7 +22,6 @@ fun Module.checkValid(globalNamespace: INameScope) { /** * todo check subroutine parameters against signature * todo check subroutine return values against target assignment values - * */ class AstChecker(private val globalNamespace: INameScope) : IAstProcessor { @@ -44,18 +43,22 @@ class AstChecker(private val globalNamespace: INameScope) : IAstProcessor { } override fun process(jump: Jump): IStatement { - super.process(jump) + if(jump.identifier!=null) { + val targetStatement = checkFunctionOrLabelExists(jump.identifier, jump) + if(targetStatement!=null) + jump.targetStatement = targetStatement // link to actual jump target + } + if(jump.address!=null && (jump.address < 0 || jump.address > 65535)) checkResult.add(SyntaxError("jump address must be valid integer 0..\$ffff", jump.position)) - return jump + return super.process(jump) } override fun process(block: Block): IStatement { if(block.address!=null && (block.address<0 || block.address>65535)) { checkResult.add(SyntaxError("block memory address must be valid integer 0..\$ffff", block.position)) } - super.process(block) - return block + return super.process(block) } /** @@ -66,9 +69,9 @@ class AstChecker(private val globalNamespace: INameScope) : IAstProcessor { checkResult.add(SyntaxError(msg, subroutine.position)) } - // subroutines may only be defined directly inside a block - if(subroutine.parent !is Block) - err("subroutines can only be defined in a block (not in other scopes)") +// // subroutines may only be defined directly inside a block @todo NAH, why should we restrict that? +// if(subroutine.parent !is Block) +// err("subroutines can only be defined in a block (not in other scopes)") if(BuiltIns.contains(subroutine.name)) err("cannot override a built-in function") @@ -79,8 +82,9 @@ class AstChecker(private val globalNamespace: INameScope) : IAstProcessor { val uniqueParamRegs = subroutine.parameters.map {it.register}.toSet() if(uniqueParamRegs.size!=subroutine.parameters.size) err("parameter registers should be unique") - val uniqueResults = subroutine.returnvalues.map {it.register}.toSet() - if(uniqueResults.size!=subroutine.returnvalues.size) + val uniqueResultRegisters = subroutine.returnvalues.filter{it.register!=null}.map {it.register.toString()}.toMutableSet() + uniqueResultRegisters.addAll(subroutine.returnvalues.filter{it.statusflag!=null}.map{it.statusflag.toString()}) + if(uniqueResultRegisters.size!=subroutine.returnvalues.size) err("return registers should be unique") super.process(subroutine) @@ -294,24 +298,28 @@ class AstChecker(private val globalNamespace: INameScope) : IAstProcessor { override fun process(functionCall: FunctionCall): IExpression { // this function call is (part of) an expression, which should be in a statement somewhere. - var statementNode: Node = functionCall - while(statementNode !is IStatement && statementNode !is ParentSentinel) - statementNode = statementNode.parent - if(statementNode is ParentSentinel) - throw FatalAstException("cannot determine statement scope of function call expression at ${functionCall.position}") + val statementNode = findParentNode(functionCall) + ?: throw FatalAstException("cannot determine statement scope of function call expression at ${functionCall.position}") - checkFunctionExists(functionCall.target, statementNode as IStatement) + val targetStatement = checkFunctionOrLabelExists(functionCall.target, statementNode) + if(targetStatement!=null) + functionCall.targetStatement = targetStatement // link to the actual target statement return super.process(functionCall) } override fun process(functionCall: FunctionCallStatement): IStatement { - checkFunctionExists(functionCall.target, functionCall) + val targetStatement = checkFunctionOrLabelExists(functionCall.target, functionCall) + if(targetStatement!=null) + functionCall.targetStatement = targetStatement // link to the actual target statement return super.process(functionCall) } - private fun checkFunctionExists(target: IdentifierReference, statement: IStatement) { - if(globalNamespace.lookup(target.nameInSource, statement)==null) - checkResult.add(SyntaxError("undefined function or subroutine: ${target.nameInSource.joinToString(".")}", statement.position)) + private fun checkFunctionOrLabelExists(target: IdentifierReference, statement: IStatement): IStatement? { + val targetStatement = globalNamespace.lookup(target.nameInSource, statement) + if(targetStatement is Label || targetStatement is Subroutine) + return targetStatement + checkResult.add(SyntaxError("undefined function or subroutine: ${target.nameInSource.joinToString(".")}", statement.position)) + return null } private fun checkValueRange(datatype: DataType, value: LiteralValue, position: Position?) : Boolean { diff --git a/il65/src/il65/ast/AstIdentifiersChecker.kt b/il65/src/il65/ast/AstIdentifiersChecker.kt index 44c547e94..383e7a87b 100644 --- a/il65/src/il65/ast/AstIdentifiersChecker.kt +++ b/il65/src/il65/ast/AstIdentifiersChecker.kt @@ -7,8 +7,8 @@ import il65.parser.ParsingFailedError * Also builds a list of all (scoped) symbol definitions */ -fun Module.checkIdentifiers(globalNamespace: INameScope): MutableMap { - val checker = AstIdentifiersChecker(globalNamespace) +fun Module.checkIdentifiers(): MutableMap { + val checker = AstIdentifiersChecker() this.process(checker) val checkResult = checker.result() checkResult.forEach { @@ -27,7 +27,7 @@ val BuiltinFunctionNames = setOf( "max", "min", "round", "rad", "deg") -class AstIdentifiersChecker(private val globalNamespace: INameScope) : IAstProcessor { +class AstIdentifiersChecker : IAstProcessor { private val checkResult: MutableList = mutableListOf() var symbols: MutableMap = mutableMapOf() @@ -42,7 +42,7 @@ class AstIdentifiersChecker(private val globalNamespace: INameScope) : IAstProce } override fun process(block: Block): IStatement { - val scopedName = block.makeScopedName(block.name).joinToString(".") + val scopedName = block.scopedname.joinToString(".") val existing = symbols[scopedName] if(existing!=null) { nameError(block.name, block.position, existing) @@ -53,7 +53,7 @@ class AstIdentifiersChecker(private val globalNamespace: INameScope) : IAstProce } override fun process(decl: VarDecl): IStatement { - val scopedName = decl.makeScopedName(decl.name).joinToString(".") + val scopedName = decl.scopedname.joinToString(".") val existing = symbols[scopedName] if(existing!=null) { nameError(decl.name, decl.position, existing) @@ -68,7 +68,7 @@ class AstIdentifiersChecker(private val globalNamespace: INameScope) : IAstProce // the special pseudo-functions can't be redefined checkResult.add(NameError("builtin function cannot be redefined", subroutine.position)) } else { - val scopedName = subroutine.makeScopedName(subroutine.name).joinToString(".") + val scopedName = subroutine.scopedname.joinToString(".") val existing = symbols[scopedName] if (existing != null) { nameError(subroutine.name, subroutine.position, existing) @@ -84,7 +84,7 @@ class AstIdentifiersChecker(private val globalNamespace: INameScope) : IAstProce // the special pseudo-functions can't be redefined checkResult.add(NameError("builtin function cannot be redefined", label.position)) } else { - val scopedName = label.makeScopedName(label.name).joinToString(".") + val scopedName = label.scopedname.joinToString(".") val existing = symbols[scopedName] if (existing != null) { nameError(label.name, label.position, existing) diff --git a/il65/src/il65/ast/AstRecursionChecker.kt b/il65/src/il65/ast/AstRecursionChecker.kt new file mode 100644 index 000000000..2cc61178f --- /dev/null +++ b/il65/src/il65/ast/AstRecursionChecker.kt @@ -0,0 +1,121 @@ +package il65.ast + +import il65.parser.ParsingFailedError + +/** + * Checks for the occurrence of recursive subroutine calls + */ + +fun Module.checkRecursion() { + val checker = AstRecursionChecker() + this.process(checker) + val checkResult = checker.result() + checkResult.forEach { + System.err.println(it) + } + if(checkResult.isNotEmpty()) + throw ParsingFailedError("There are ${checkResult.size} errors in module '$name'.") +} + + +class DirectedGraph { + private val graph = mutableMapOf>() + private var uniqueVertices = mutableSetOf() + val numVertices : Int + get() = uniqueVertices.size + + fun add(from: VT, to: VT) { + var targets = graph[from] + if(targets==null) { + targets = mutableSetOf() + graph[from] = targets + } + targets.add(to) + uniqueVertices.add(from) + uniqueVertices.add(to) + } + + fun print() { + println("#vertices: $numVertices") + graph.forEach { from, to -> + println("$from CALLS:") + to.forEach { it -> println(" $it") } + } + val cycle = checkForCycle() + if(cycle.isNotEmpty()) { + println("CYCLIC! $cycle") + } + } + + fun checkForCycle(): MutableList { + val visited = uniqueVertices.associate { it to false }.toMutableMap() + val recStack = uniqueVertices.associate { it to false }.toMutableMap() + val cycle = mutableListOf() + for(node in uniqueVertices) { + if(isCyclicUntil(node, visited, recStack, cycle)) + return cycle + } + return mutableListOf() + } + + private fun isCyclicUntil(node: VT, + visited: MutableMap, + recStack: MutableMap, + cycleNodes: MutableList): Boolean { + + if(recStack[node]==true) return true + if(visited[node]==true) return false + + // mark current node as visited and add to recursion stack + visited[node] = true + recStack[node] = true + + // recurse for all neighbours + val neighbors = graph[node] + if(neighbors!=null) { + for (neighbour in neighbors) { + if (isCyclicUntil(neighbour, visited, recStack, cycleNodes)) { + cycleNodes.add(node) + return true + } + } + } + + // pop node from recursion stack + recStack[node] = false + return false + } +} + + +class AstRecursionChecker : IAstProcessor { + private val callGraph = DirectedGraph() + + fun result(): List { + val cycle = callGraph.checkForCycle() + if(cycle.isEmpty()) + return emptyList() + val chain = cycle.joinToString(" <-- ") { "${it.name} at ${it.position}" } + return listOf(AstException("Program contains recursive subroutine calls, this is not supported. Recursive chain:\n"+chain)) + } + + override fun process(functionCall: FunctionCallStatement): IStatement { + val scope = functionCall.definingScope() + val targetScope = when(functionCall.targetStatement) { + is Subroutine -> functionCall.targetStatement as Subroutine + else -> functionCall.targetStatement.definingScope() + } + callGraph.add(scope, targetScope) + return super.process(functionCall) + } + + override fun process(functionCall: FunctionCall): IExpression { + val scope = functionCall.definingScope() + val targetScope = when(functionCall.targetStatement) { + is Subroutine -> functionCall.targetStatement as Subroutine + else -> functionCall.targetStatement.definingScope() + } + callGraph.add(scope, targetScope) + return super.process(functionCall) + } +} diff --git a/il65/src/il65/functions/BuiltinFunctions.kt b/il65/src/il65/functions/BuiltinFunctions.kt index 577cfc010..2deeb8ade 100644 --- a/il65/src/il65/functions/BuiltinFunctions.kt +++ b/il65/src/il65/functions/BuiltinFunctions.kt @@ -6,6 +6,9 @@ import il65.ast.* val BuiltIns = listOf("sin", "cos", "abs", "acos", "asin", "tan", "atan", "log", "log10", "sqrt", "max", "min", "round", "rad", "deg") +// @todo additional builtins such as: avg, sum, abs, round + + class NotConstArgumentException: AstException("not a const argument to a built-in function") diff --git a/il65/src/il65/optimizing/StatementsOptimizer.kt b/il65/src/il65/optimizing/StatementsOptimizer.kt index b14280bf5..a25c6ac66 100644 --- a/il65/src/il65/optimizing/StatementsOptimizer.kt +++ b/il65/src/il65/optimizing/StatementsOptimizer.kt @@ -38,31 +38,28 @@ class StatementOptimizer(private val globalNamespace: INameScope) : IAstProcesso } override fun process(functionCall: FunctionCall): IExpression { - val function = globalNamespace.lookup(functionCall.target.nameInSource, functionCall) - if(function!=null) { - val scopedName = when(function) { - is Label -> function.makeScopedName(function.name) - is Subroutine -> function.makeScopedName(function.name) - else -> throw AstException("invalid function call target node type") - } - globalNamespace.registerUsedName(scopedName.joinToString(".")) - } + val target = globalNamespace.lookup(functionCall.target.nameInSource, functionCall) + if(target!=null) + used(target) return super.process(functionCall) } override fun process(functionCall: FunctionCallStatement): IStatement { - val function = globalNamespace.lookup(functionCall.target.nameInSource, functionCall) - if(function!=null) { - val scopedName = when(function) { - is Label -> function.makeScopedName(function.name) - is Subroutine -> function.makeScopedName(function.name) - else -> throw AstException("invalid function call target node type") - } - globalNamespace.registerUsedName(scopedName.joinToString(".")) - } + val target = globalNamespace.lookup(functionCall.target.nameInSource, functionCall) + if(target!=null) + used(target) return super.process(functionCall) } + override fun process(jump: Jump): IStatement { + if(jump.identifier!=null) { + val target = globalNamespace.lookup(jump.identifier.nameInSource, jump) + if (target != null) + used(target) + } + return super.process(jump) + } + override fun process(ifStatement: IfStatement): IStatement { super.process(ifStatement) val constvalue = ifStatement.condition.constValue(globalNamespace) @@ -80,6 +77,15 @@ class StatementOptimizer(private val globalNamespace: INameScope) : IAstProcesso return ifStatement } + private fun used(stmt: IStatement) { + val scopedName = when (stmt) { + is Label -> stmt.scopedname + is Subroutine -> stmt.scopedname + else -> throw AstException("invalid call target node type: ${stmt::class}") + } + globalNamespace.registerUsedName(scopedName.joinToString(".")) + } + fun removeUnusedNodes(usedNames: Set, allScopedSymbolDefinitions: MutableMap) { for ((name, value) in allScopedSymbolDefinitions) { if(!usedNames.contains(name)) {