mirror of
https://github.com/irmen/prog8.git
synced 2024-06-10 05:29:29 +00:00
1519 lines
74 KiB
Kotlin
1519 lines
74 KiB
Kotlin
package prog8.compiler.astprocessing
|
|
|
|
import prog8.ast.*
|
|
import prog8.ast.base.FatalAstException
|
|
import prog8.ast.base.SyntaxError
|
|
import prog8.ast.expressions.*
|
|
import prog8.ast.statements.*
|
|
import prog8.ast.walk.IAstVisitor
|
|
import prog8.code.core.*
|
|
import prog8.compilerinterface.*
|
|
import java.io.CharConversionException
|
|
import java.io.File
|
|
import kotlin.io.path.Path
|
|
|
|
/**
|
|
* Semantic analysis.
|
|
*/
|
|
internal class AstChecker(private val program: Program,
|
|
private val errors: IErrorReporter,
|
|
private val compilerOptions: CompilationOptions
|
|
) : IAstVisitor {
|
|
|
|
override fun visit(program: Program) {
|
|
require(program === this.program)
|
|
// there must be a single 'main' block with a 'start' subroutine for the program entry point.
|
|
val mainBlocks = program.modules.flatMap { it.statements }.filter { b -> b is Block && b.name=="main" }.map { it as Block }
|
|
if(mainBlocks.size>1)
|
|
errors.err("more than one 'main' block", mainBlocks[0].position)
|
|
if(mainBlocks.isEmpty())
|
|
errors.err("there is no 'main' block", program.modules.firstOrNull()?.position ?: Position.DUMMY)
|
|
|
|
for(mainBlock in mainBlocks) {
|
|
val startSub = mainBlock.subScope("start") as? Subroutine
|
|
if (startSub == null) {
|
|
errors.err("missing program entrypoint ('start' subroutine in 'main' block)", mainBlock.position)
|
|
} else {
|
|
if (startSub.parameters.isNotEmpty() || startSub.returntypes.isNotEmpty())
|
|
errors.err("program entrypoint subroutine can't have parameters and/or return values", startSub.position)
|
|
}
|
|
}
|
|
|
|
if(compilerOptions.floats) {
|
|
if (compilerOptions.zeropage !in arrayOf(ZeropageType.FLOATSAFE, ZeropageType.BASICSAFE, ZeropageType.DONTUSE ))
|
|
errors.err("when floats are enabled, zero page type should be 'floatsafe' or 'basicsafe' or 'dontuse'", program.toplevelModule.position)
|
|
}
|
|
|
|
super.visit(program)
|
|
}
|
|
|
|
override fun visit(module: Module) {
|
|
super.visit(module)
|
|
val directives = module.statements.filterIsInstance<Directive>().groupBy { it.directive }
|
|
directives.filter { it.value.size > 1 }.forEach{ entry ->
|
|
when(entry.key) {
|
|
"%output", "%launcher", "%zeropage", "%address" ->
|
|
entry.value.forEach { errors.err("directive can just occur once", it.position) }
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun visit(returnStmt: Return) {
|
|
val expectedReturnValues = returnStmt.definingSubroutine?.returntypes ?: emptyList()
|
|
if(expectedReturnValues.size>1) {
|
|
throw FatalAstException("cannot use a return with one value in a subroutine that has multiple return values: $returnStmt")
|
|
}
|
|
|
|
if(expectedReturnValues.isEmpty() && returnStmt.value!=null) {
|
|
errors.err("invalid number of return values", returnStmt.position)
|
|
}
|
|
if(expectedReturnValues.isNotEmpty() && returnStmt.value==null) {
|
|
errors.err("invalid number of return values", returnStmt.position)
|
|
}
|
|
if(expectedReturnValues.size==1 && returnStmt.value!=null) {
|
|
val valueDt = returnStmt.value!!.inferType(program)
|
|
if(!valueDt.isKnown) {
|
|
errors.err("return value type mismatch or unknown symbol", returnStmt.value!!.position)
|
|
} else {
|
|
if (expectedReturnValues[0] != valueDt.getOr(DataType.UNDEFINED))
|
|
errors.err("type $valueDt of return value doesn't match subroutine's return type ${expectedReturnValues[0]}", returnStmt.value!!.position)
|
|
}
|
|
}
|
|
super.visit(returnStmt)
|
|
}
|
|
|
|
override fun visit(ifElse: IfElse) {
|
|
if(!ifElse.condition.inferType(program).isInteger)
|
|
errors.err("condition value should be an integer type", ifElse.condition.position)
|
|
super.visit(ifElse)
|
|
}
|
|
|
|
override fun visit(forLoop: ForLoop) {
|
|
|
|
fun checkUnsignedLoopDownto0(range: RangeExpression?) {
|
|
if(range==null)
|
|
return
|
|
val step = range.step.constValue(program)?.number ?: 1.0
|
|
if(step < -1.0) {
|
|
val limit = range.to.constValue(program)?.number
|
|
if(limit==0.0 && range.from.constValue(program)==null)
|
|
errors.err("for unsigned loop variable it's not possible to count down with step != -1 from a non-const value to exactly zero due to value wrapping", forLoop.position)
|
|
}
|
|
}
|
|
|
|
val iterableDt = forLoop.iterable.inferType(program).getOr(DataType.BYTE)
|
|
if(iterableDt !in IterableDatatypes && forLoop.iterable !is RangeExpression) {
|
|
errors.err("can only loop over an iterable type", forLoop.position)
|
|
} else {
|
|
val loopvar = forLoop.loopVar.targetVarDecl(program)
|
|
if(loopvar==null || loopvar.type== VarDeclType.CONST) {
|
|
errors.err("for loop requires a variable to loop with", forLoop.position)
|
|
} else {
|
|
when (loopvar.datatype) {
|
|
DataType.UBYTE -> {
|
|
if(iterableDt!= DataType.UBYTE && iterableDt!= DataType.ARRAY_UB && iterableDt != DataType.STR)
|
|
errors.err("ubyte loop variable can only loop over unsigned bytes or strings", forLoop.position)
|
|
|
|
checkUnsignedLoopDownto0(forLoop.iterable as? RangeExpression)
|
|
}
|
|
DataType.UWORD -> {
|
|
if(iterableDt!= DataType.UBYTE && iterableDt!= DataType.UWORD && iterableDt != DataType.STR &&
|
|
iterableDt != DataType.ARRAY_UB && iterableDt!= DataType.ARRAY_UW)
|
|
errors.err("uword loop variable can only loop over unsigned bytes, words or strings", forLoop.position)
|
|
|
|
checkUnsignedLoopDownto0(forLoop.iterable as? RangeExpression)
|
|
}
|
|
DataType.BYTE -> {
|
|
if(iterableDt!= DataType.BYTE && iterableDt!= DataType.ARRAY_B)
|
|
errors.err("byte loop variable can only loop over bytes", forLoop.position)
|
|
}
|
|
DataType.WORD -> {
|
|
if(iterableDt!= DataType.BYTE && iterableDt!= DataType.WORD &&
|
|
iterableDt != DataType.ARRAY_B && iterableDt!= DataType.ARRAY_W)
|
|
errors.err("word loop variable can only loop over bytes or words", forLoop.position)
|
|
}
|
|
DataType.FLOAT -> {
|
|
// Looping over float variables is very inefficient because the loopvar is going to
|
|
// get copied over with new values all the time. We don't support this for now.
|
|
// Loop with an integer index variable if you really need to... or write different code.
|
|
errors.err("for loop only supports integers", forLoop.position)
|
|
}
|
|
else -> errors.err("loop variable must be numeric type", forLoop.position)
|
|
}
|
|
if(errors.noErrors()) {
|
|
// check loop range values
|
|
val range = forLoop.iterable as? RangeExpression
|
|
if(range!=null) {
|
|
val from = range.from as? NumericLiteral
|
|
val to = range.to as? NumericLiteral
|
|
if(from != null)
|
|
checkValueTypeAndRange(loopvar.datatype, from)
|
|
else if(range.from.inferType(program) isnot loopvar.datatype)
|
|
errors.err("range start value is incompatible with loop variable type", range.position)
|
|
if(to != null)
|
|
checkValueTypeAndRange(loopvar.datatype, to)
|
|
else if(range.to.inferType(program) isnot loopvar.datatype)
|
|
errors.err("range end value is incompatible with loop variable type", range.position)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
super.visit(forLoop)
|
|
}
|
|
|
|
override fun visit(jump: Jump) {
|
|
val ident = jump.identifier
|
|
if(ident!=null) {
|
|
val targetStatement = checkFunctionOrLabelExists(ident, jump)
|
|
if(targetStatement!=null) {
|
|
if(targetStatement is BuiltinFunctionPlaceholder)
|
|
errors.err("can't jump to a builtin function", jump.position)
|
|
}
|
|
if(targetStatement is Subroutine && targetStatement.parameters.any()) {
|
|
errors.err("can't jump to a subroutine that takes parameters", jump.position)
|
|
}
|
|
}
|
|
|
|
val addr = jump.address
|
|
if(addr!=null && addr > 65535u)
|
|
errors.err("jump address must be valid integer 0..\$ffff", jump.position)
|
|
super.visit(jump)
|
|
}
|
|
|
|
override fun visit(gosub: GoSub) {
|
|
val ident = gosub.identifier
|
|
if(ident!=null) {
|
|
val targetStatement = checkFunctionOrLabelExists(ident, gosub)
|
|
if(targetStatement!=null) {
|
|
if(targetStatement is BuiltinFunctionPlaceholder)
|
|
errors.err("can't gosub to a builtin function", gosub.position)
|
|
}
|
|
}
|
|
|
|
val addr = gosub.address
|
|
if(addr!=null && addr > 65535u)
|
|
errors.err("gosub address must be valid integer 0..\$ffff", gosub.position)
|
|
super.visit(gosub)
|
|
}
|
|
|
|
override fun visit(block: Block) {
|
|
val addr = block.address
|
|
if(addr!=null && addr>65535u) {
|
|
errors.err("block memory address must be valid integer 0..\$ffff", block.position)
|
|
}
|
|
|
|
for (statement in block.statements) {
|
|
val ok = when (statement) {
|
|
is Block,
|
|
is Directive,
|
|
is Label,
|
|
is VarDecl,
|
|
is InlineAssembly,
|
|
is IStatementContainer -> true
|
|
is Assignment -> {
|
|
val target = statement.target.identifier!!.targetStatement(program)
|
|
target === statement.previousSibling() // an initializer assignment is okay
|
|
}
|
|
else -> false
|
|
}
|
|
if (!ok) {
|
|
errors.err("non-declarative statement occurs in block scope, where it will never be executed. Move it to a subroutine instead.", statement.position)
|
|
break
|
|
}
|
|
}
|
|
|
|
super.visit(block)
|
|
}
|
|
|
|
override fun visit(label: Label) {
|
|
// scope check
|
|
if(label.parent !is Block && label.parent !is Subroutine && label.parent !is AnonymousScope) {
|
|
errors.err("Labels can only be defined in the scope of a block, a loop body, or within another subroutine", label.position)
|
|
}
|
|
super.visit(label)
|
|
}
|
|
|
|
private fun hasReturnOrJumpOrRts(scope: IStatementContainer): Boolean {
|
|
class Searcher: IAstVisitor
|
|
{
|
|
var count=0
|
|
|
|
override fun visit(returnStmt: Return) {
|
|
count++
|
|
}
|
|
override fun visit(jump: Jump) {
|
|
count++
|
|
}
|
|
|
|
override fun visit(inlineAssembly: InlineAssembly) {
|
|
val assembly = inlineAssembly.assembly
|
|
if(" rti" in assembly || "\trti" in assembly || " rts" in assembly || "\trts" in assembly ||
|
|
" jmp" in assembly || "\tjmp" in assembly || " bra" in assembly || "\tbra" in assembly )
|
|
count++
|
|
}
|
|
}
|
|
|
|
val s=Searcher()
|
|
for(stmt in scope.statements) {
|
|
stmt.accept(s)
|
|
if(s.count>0)
|
|
return true
|
|
}
|
|
return s.count > 0
|
|
}
|
|
|
|
override fun visit(subroutine: Subroutine) {
|
|
fun err(msg: String) = errors.err(msg, subroutine.position)
|
|
|
|
if(subroutine.name in BuiltinFunctions)
|
|
err("cannot redefine a built-in function")
|
|
|
|
if(subroutine.parameters.size>6 && !subroutine.isAsmSubroutine)
|
|
errors.warn("subroutine has a large number of parameters, this slows down code execution a lot", subroutine.position)
|
|
|
|
val uniqueNames = subroutine.parameters.asSequence().map { it.name }.toSet()
|
|
if(uniqueNames.size!=subroutine.parameters.size)
|
|
err("parameter names must be unique")
|
|
|
|
super.visit(subroutine)
|
|
|
|
// user-defined subroutines can only have zero or one return type
|
|
// (multiple return values are only allowed for asm subs)
|
|
if(!subroutine.isAsmSubroutine && subroutine.returntypes.size>1)
|
|
err("subroutines can only have one return value")
|
|
|
|
// subroutine must contain at least one 'return' or 'goto'
|
|
// (or if it has an asm block, that must contain a 'rts' or 'jmp' or 'bra')
|
|
if(!hasReturnOrJumpOrRts(subroutine)) {
|
|
if (subroutine.returntypes.isNotEmpty()) {
|
|
// for asm subroutines with an address, no statement check is possible.
|
|
if (subroutine.asmAddress == null && !subroutine.inline)
|
|
err("non-inline subroutine has result value(s) and thus must have at least one 'return' or 'goto' in it (or rts/jmp/bra in case of %asm)")
|
|
}
|
|
}
|
|
|
|
if(subroutine.inline && !subroutine.isAsmSubroutine)
|
|
err("subroutine inlining is currently only supported on asmsub routines")
|
|
|
|
if(subroutine.parent !is Block && subroutine.parent !is Subroutine)
|
|
err("subroutines can only be defined in the scope of a block or within another subroutine")
|
|
|
|
if(subroutine.isAsmSubroutine) {
|
|
if(subroutine.asmParameterRegisters.size != subroutine.parameters.size)
|
|
err("number of asm parameter registers is not the isSameAs as number of parameters")
|
|
if(subroutine.asmReturnvaluesRegisters.size != subroutine.returntypes.size)
|
|
err("number of return registers is not the isSameAs as number of return values")
|
|
for(param in subroutine.parameters.zip(subroutine.asmParameterRegisters)) {
|
|
if(param.second.registerOrPair in arrayOf(RegisterOrPair.A, RegisterOrPair.X, RegisterOrPair.Y)) {
|
|
if (param.first.type != DataType.UBYTE && param.first.type != DataType.BYTE)
|
|
err("parameter '${param.first.name}' should be (u)byte")
|
|
}
|
|
else if(param.second.registerOrPair in arrayOf(RegisterOrPair.AX, RegisterOrPair.AY, RegisterOrPair.XY)) {
|
|
if (param.first.type != DataType.UWORD && param.first.type != DataType.WORD
|
|
&& param.first.type != DataType.STR && param.first.type !in ArrayDatatypes && param.first.type != DataType.FLOAT)
|
|
err("parameter '${param.first.name}' should be (u)word (an address) or str")
|
|
}
|
|
else if(param.second.statusflag!=null) {
|
|
if (param.first.type != DataType.UBYTE)
|
|
err("parameter '${param.first.name}' should be ubyte")
|
|
}
|
|
}
|
|
subroutine.returntypes.zip(subroutine.asmReturnvaluesRegisters).forEachIndexed { index, pair ->
|
|
if(pair.second.registerOrPair in arrayOf(RegisterOrPair.A, RegisterOrPair.X, RegisterOrPair.Y)) {
|
|
if (pair.first != DataType.UBYTE && pair.first != DataType.BYTE)
|
|
err("return value #${index + 1} should be (u)byte")
|
|
}
|
|
else if(pair.second.registerOrPair in arrayOf(RegisterOrPair.AX, RegisterOrPair.AY, RegisterOrPair.XY)) {
|
|
if (pair.first != DataType.UWORD && pair.first != DataType.WORD
|
|
&& pair.first != DataType.STR && pair.first !in ArrayDatatypes && pair.first != DataType.FLOAT)
|
|
err("return value #${index + 1} should be (u)word/address")
|
|
}
|
|
else if(pair.second.statusflag!=null) {
|
|
if (pair.first != DataType.UBYTE)
|
|
err("return value #${index + 1} should be ubyte")
|
|
}
|
|
}
|
|
|
|
val regCounts = mutableMapOf<CpuRegister, Int>().withDefault { 0 }
|
|
val statusflagCounts = mutableMapOf<Statusflag, Int>().withDefault { 0 }
|
|
fun countRegisters(from: Iterable<RegisterOrStatusflag>) {
|
|
regCounts.clear()
|
|
statusflagCounts.clear()
|
|
for(p in from) {
|
|
when(p.registerOrPair) {
|
|
RegisterOrPair.A -> regCounts[CpuRegister.A]=regCounts.getValue(CpuRegister.A)+1
|
|
RegisterOrPair.X -> regCounts[CpuRegister.X]=regCounts.getValue(CpuRegister.X)+1
|
|
RegisterOrPair.Y -> regCounts[CpuRegister.Y]=regCounts.getValue(CpuRegister.Y)+1
|
|
RegisterOrPair.AX -> {
|
|
regCounts[CpuRegister.A]=regCounts.getValue(CpuRegister.A)+1
|
|
regCounts[CpuRegister.X]=regCounts.getValue(CpuRegister.X)+1
|
|
}
|
|
RegisterOrPair.AY -> {
|
|
regCounts[CpuRegister.A]=regCounts.getValue(CpuRegister.A)+1
|
|
regCounts[CpuRegister.Y]=regCounts.getValue(CpuRegister.Y)+1
|
|
}
|
|
RegisterOrPair.XY -> {
|
|
regCounts[CpuRegister.X]=regCounts.getValue(CpuRegister.X)+1
|
|
regCounts[CpuRegister.Y]=regCounts.getValue(CpuRegister.Y)+1
|
|
}
|
|
RegisterOrPair.FAC1, RegisterOrPair.FAC2 -> { /* no sensible way to count this */ }
|
|
RegisterOrPair.R0,
|
|
RegisterOrPair.R1,
|
|
RegisterOrPair.R2,
|
|
RegisterOrPair.R3,
|
|
RegisterOrPair.R4,
|
|
RegisterOrPair.R5,
|
|
RegisterOrPair.R6,
|
|
RegisterOrPair.R7,
|
|
RegisterOrPair.R8,
|
|
RegisterOrPair.R9,
|
|
RegisterOrPair.R10,
|
|
RegisterOrPair.R11,
|
|
RegisterOrPair.R12,
|
|
RegisterOrPair.R13,
|
|
RegisterOrPair.R14,
|
|
RegisterOrPair.R15 -> { /* no sensible way to count this */ }
|
|
null -> {
|
|
val statusf = p.statusflag
|
|
if (statusf != null)
|
|
statusflagCounts[statusf] = statusflagCounts.getValue(statusf) + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
countRegisters(subroutine.asmParameterRegisters)
|
|
if(regCounts.any{it.value>1})
|
|
err("a register is used multiple times in the parameters")
|
|
if(statusflagCounts.any{it.value>1})
|
|
err("a status flag is used multiple times in the parameters")
|
|
countRegisters(subroutine.asmReturnvaluesRegisters)
|
|
if(regCounts.any{it.value>1})
|
|
err("a register is used multiple times in the return values")
|
|
if(statusflagCounts.any{it.value>1})
|
|
err("a status flag is used multiple times in the return values")
|
|
|
|
if(subroutine.asmClobbers.intersect(regCounts.keys).isNotEmpty())
|
|
err("a return register is also in the clobber list")
|
|
|
|
if(subroutine.statements.any{it !is InlineAssembly})
|
|
err("asmsub can only contain inline assembly (%asm)")
|
|
|
|
val statusFlagsNoCarry = subroutine.asmParameterRegisters.mapNotNull { it.statusflag }.toSet() - Statusflag.Pc
|
|
if(statusFlagsNoCarry.isNotEmpty())
|
|
err("can only use Carry as status flag parameter")
|
|
|
|
}
|
|
|
|
// Non-string and non-ubytearray Pass-by-reference datatypes can not occur as parameters to a subroutine directly
|
|
// Instead, their reference (address) should be passed (as an UWORD).
|
|
for(p in subroutine.parameters) {
|
|
if(p.type in PassByReferenceDatatypes && p.type !in listOf(DataType.STR, DataType.ARRAY_UB)) {
|
|
errors.err("this pass-by-reference type can't be used as a parameter type. Instead, use an uword to receive the address, or access the variable from the outer scope directly.", p.position)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun visit(untilLoop: UntilLoop) {
|
|
if(!untilLoop.condition.inferType(program).isInteger)
|
|
errors.err("condition value should be an integer type", untilLoop.condition.position)
|
|
super.visit(untilLoop)
|
|
}
|
|
|
|
override fun visit(whileLoop: WhileLoop) {
|
|
if(!whileLoop.condition.inferType(program).isInteger)
|
|
errors.err("condition value should be an integer type", whileLoop.condition.position)
|
|
super.visit(whileLoop)
|
|
}
|
|
|
|
override fun visit(repeatLoop: RepeatLoop) {
|
|
val iterations = repeatLoop.iterations?.constValue(program)
|
|
if(iterations != null && iterations.number.toInt() > 65535)
|
|
errors.err("repeat cannot go over 65535 iterations", iterations.position)
|
|
super.visit(repeatLoop)
|
|
}
|
|
|
|
override fun visit(assignment: Assignment) {
|
|
val targetDt = assignment.target.inferType(program)
|
|
val valueDt = assignment.value.inferType(program)
|
|
if(valueDt.isKnown && !(valueDt isAssignableTo targetDt)) {
|
|
if(targetDt.isIterable)
|
|
errors.err("cannot assign value to string or array", assignment.value.position)
|
|
else if(!(valueDt istype DataType.STR && targetDt istype DataType.UWORD)) {
|
|
if(targetDt.isUnknown) {
|
|
if(assignment.target.identifier?.targetStatement(program)!=null)
|
|
errors.err("target datatype is unknown", assignment.target.position)
|
|
// otherwise, another error about missing symbol is already reported.
|
|
} else {
|
|
errors.err("type of value $valueDt doesn't match target $targetDt", assignment.value.position)
|
|
}
|
|
}
|
|
}
|
|
|
|
if(assignment.value is TypecastExpression) {
|
|
if(assignment.isAugmentable && targetDt istype DataType.FLOAT)
|
|
errors.err("typecasting a float value in-place makes no sense", assignment.value.position)
|
|
}
|
|
|
|
val numvalue = assignment.value.constValue(program)
|
|
if(numvalue!=null && targetDt.isKnown)
|
|
checkValueTypeAndRange(targetDt.getOr(DataType.UNDEFINED), numvalue)
|
|
|
|
super.visit(assignment)
|
|
}
|
|
|
|
override fun visit(assignTarget: AssignTarget) {
|
|
super.visit(assignTarget)
|
|
|
|
val memAddr = assignTarget.memoryAddress?.addressExpression?.constValue(program)?.number?.toInt()
|
|
if (memAddr != null) {
|
|
if (memAddr < 0 || memAddr >= 65536)
|
|
errors.err("address out of range", assignTarget.position)
|
|
}
|
|
|
|
val assignment = assignTarget.parent as Statement
|
|
val targetIdentifier = assignTarget.identifier
|
|
if (targetIdentifier != null) {
|
|
val targetName = targetIdentifier.nameInSource
|
|
when (val targetSymbol = assignment.definingScope.lookup(targetName)) {
|
|
null -> {
|
|
errors.err("undefined symbol: ${targetIdentifier.nameInSource.joinToString(".")}", targetIdentifier.position)
|
|
return
|
|
}
|
|
!is VarDecl -> {
|
|
errors.err("assignment LHS must be register or variable", assignment.position)
|
|
return
|
|
}
|
|
else -> {
|
|
if (targetSymbol.type == VarDeclType.CONST) {
|
|
errors.err("cannot assign new value to a constant", assignment.position)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (assignment is Assignment) {
|
|
val targetDatatype = assignTarget.inferType(program)
|
|
if (targetDatatype.isKnown) {
|
|
val constVal = assignment.value.constValue(program)
|
|
if(constVal==null) {
|
|
val sourceDatatype = assignment.value.inferType(program)
|
|
if (sourceDatatype.isUnknown) {
|
|
if (assignment.value !is FunctionCallExpression)
|
|
errors.err("assignment value is invalid or has no proper datatype, maybe forgot '&' (address-of)", assignment.value.position)
|
|
} else {
|
|
checkAssignmentCompatible(targetDatatype.getOr(DataType.BYTE),
|
|
sourceDatatype.getOr(DataType.BYTE), assignment.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun visit(addressOf: AddressOf) {
|
|
val variable=addressOf.identifier.targetVarDecl(program)
|
|
if(variable!=null && variable.type==VarDeclType.CONST)
|
|
errors.err("invalid pointer-of operand type", addressOf.position)
|
|
super.visit(addressOf)
|
|
}
|
|
|
|
override fun visit(decl: VarDecl) {
|
|
fun err(msg: String) = errors.err(msg, decl.position)
|
|
|
|
// the initializer value can't refer to the variable itself (recursive definition)
|
|
if(decl.value?.referencesIdentifier(listOf(decl.name)) == true || decl.arraysize?.indexExpr?.referencesIdentifier(listOf(decl.name)) == true)
|
|
err("recursive var declaration")
|
|
|
|
// CONST can only occur on simple types (byte, word, float)
|
|
if(decl.type== VarDeclType.CONST) {
|
|
if (decl.datatype !in NumericDatatypes)
|
|
err("const modifier can only be used on numeric types (byte, word, float)")
|
|
}
|
|
|
|
// FLOATS enabled?
|
|
if(!compilerOptions.floats && decl.datatype.oneOf(DataType.FLOAT, DataType.ARRAY_F) && decl.type!= VarDeclType.MEMORY)
|
|
err("floating point used, but that is not enabled via options")
|
|
|
|
// ARRAY without size specifier MUST have an iterable initializer value
|
|
if(decl.isArray && decl.arraysize==null) {
|
|
if(decl.type== VarDeclType.MEMORY)
|
|
err("memory mapped array must have a size specification")
|
|
if(decl.value==null) {
|
|
err("array variable is missing a size specification or an initialization value")
|
|
return
|
|
}
|
|
if(decl.value is NumericLiteral) {
|
|
err("unsized array declaration cannot use a single literal initialization value")
|
|
return
|
|
}
|
|
if(decl.value is RangeExpression)
|
|
throw InternalCompilerException("range expressions in vardecls should have been converted into array values during constFolding $decl")
|
|
}
|
|
|
|
when(decl.type) {
|
|
VarDeclType.VAR, VarDeclType.CONST -> {
|
|
when(decl.value) {
|
|
null -> {
|
|
// a vardecl without an initial value, don't bother with it
|
|
}
|
|
is RangeExpression -> throw InternalCompilerException("range expression should have been converted to a true array value")
|
|
is StringLiteral -> {
|
|
checkValueTypeAndRangeString(decl.datatype, decl.value as StringLiteral)
|
|
}
|
|
is ArrayLiteral -> {
|
|
val arraySpec = decl.arraysize ?: ArrayIndex.forArray(decl.value as ArrayLiteral)
|
|
checkValueTypeAndRangeArray(decl.datatype, arraySpec, decl.value as ArrayLiteral)
|
|
}
|
|
is NumericLiteral -> {
|
|
checkValueTypeAndRange(decl.datatype, decl.value as NumericLiteral)
|
|
}
|
|
else -> {
|
|
if(decl.type==VarDeclType.CONST) {
|
|
err("const declaration needs a compile-time constant initializer value, or range")
|
|
super.visit(decl)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
VarDeclType.MEMORY -> {
|
|
val arraysize = decl.arraysize
|
|
if(arraysize!=null) {
|
|
val arraySize = arraysize.constIndex() ?: 1
|
|
when(decl.datatype) {
|
|
DataType.ARRAY_B, DataType.ARRAY_UB ->
|
|
if(arraySize > 256)
|
|
err("byte array length must be 1-256")
|
|
DataType.ARRAY_W, DataType.ARRAY_UW ->
|
|
if(arraySize > 128)
|
|
err("word array length must be 1-128")
|
|
DataType.ARRAY_F ->
|
|
if(arraySize > 51)
|
|
err("float array length must be 1-51")
|
|
else -> {}
|
|
}
|
|
}
|
|
val numvalue = decl.value as? NumericLiteral
|
|
if(numvalue!=null) {
|
|
if (numvalue.type !in IntegerDatatypes || numvalue.number.toInt() < 0 || numvalue.number.toInt() > 65535) {
|
|
err("memory address must be valid integer 0..\$ffff")
|
|
}
|
|
} else {
|
|
err("value of memory mapped variable can only be a fixed number, perhaps you meant to use an address pointer type instead?")
|
|
}
|
|
}
|
|
}
|
|
|
|
val declValue = decl.value
|
|
if(declValue!=null && decl.type==VarDeclType.VAR) {
|
|
if (declValue.inferType(program) isnot decl.datatype) {
|
|
err("initialisation value has incompatible type (${declValue.inferType(program)}) for the variable (${decl.datatype})")
|
|
}
|
|
}
|
|
|
|
// array length limits and constant lenghts
|
|
if(decl.isArray) {
|
|
val length = decl.arraysize?.constIndex()
|
|
if(length==null)
|
|
err("array length must be known at compile-time")
|
|
else {
|
|
when (decl.datatype) {
|
|
DataType.STR, DataType.ARRAY_UB, DataType.ARRAY_B -> {
|
|
if (length == 0 || length > 256)
|
|
err("string and byte array length must be 1-256")
|
|
}
|
|
DataType.ARRAY_UW, DataType.ARRAY_W -> {
|
|
if (length == 0 || length > 128)
|
|
err("word array length must be 1-128")
|
|
}
|
|
DataType.ARRAY_F -> {
|
|
if (length == 0 || length > 51)
|
|
err("float array length must be 1-51")
|
|
}
|
|
else -> {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(decl.datatype==DataType.STR) {
|
|
if(decl.value==null) {
|
|
// complain about uninitialized str, but only if it's a regular variable
|
|
val parameter = (decl.parent as? Subroutine)?.parameters?.singleOrNull{ it.name==decl.name }
|
|
if(parameter==null)
|
|
err("string var must be initialized with a string literal")
|
|
}
|
|
}
|
|
|
|
if(compilerOptions.zeropage==ZeropageType.DONTUSE && decl.zeropage == ZeropageWish.REQUIRE_ZEROPAGE)
|
|
err("zeropage usage has been disabled by options")
|
|
|
|
super.visit(decl)
|
|
}
|
|
|
|
override fun visit(directive: Directive) {
|
|
fun err(msg: String) {
|
|
errors.err(msg, directive.position)
|
|
}
|
|
when(directive.directive) {
|
|
"%output" -> {
|
|
if(directive.parent !is Module)
|
|
err("this directive may only occur at module level")
|
|
if(directive.args.size!=1 || directive.args[0].name != "raw" && directive.args[0].name != "prg")
|
|
err("invalid output directive type, expected raw or prg")
|
|
}
|
|
"%launcher" -> {
|
|
if(directive.parent !is Module)
|
|
err("this directive may only occur at module level")
|
|
if(directive.args.size!=1 || directive.args[0].name != "basic" && directive.args[0].name != "none")
|
|
err("invalid launcher directive type, expected basic or none")
|
|
}
|
|
"%zeropage" -> {
|
|
if(directive.parent !is Module)
|
|
err("this directive may only occur at module level")
|
|
if(directive.args.size!=1 ||
|
|
directive.args[0].name != "basicsafe" &&
|
|
directive.args[0].name != "floatsafe" &&
|
|
directive.args[0].name != "kernalsafe" &&
|
|
directive.args[0].name != "dontuse" &&
|
|
directive.args[0].name != "full")
|
|
err("invalid zp type, expected basicsafe, floatsafe, kernalsafe, dontuse, or full")
|
|
}
|
|
"%zpreserved" -> {
|
|
if(directive.parent !is Module)
|
|
err("this directive may only occur at module level")
|
|
if(directive.args.size!=2 || directive.args[0].int==null || directive.args[1].int==null)
|
|
err("requires two addresses (start, end)")
|
|
if(directive.args[0].int!! > 255u || directive.args[1].int!! > 255u)
|
|
err("start and end addresss must be in Zeropage so 0..255")
|
|
}
|
|
"%address" -> {
|
|
if(directive.parent !is Module)
|
|
err("this directive may only occur at module level")
|
|
if(directive.args.size!=1 || directive.args[0].int == null)
|
|
err("invalid address directive, expected numeric address argument")
|
|
}
|
|
"%import" -> {
|
|
if(directive.parent !is Module)
|
|
err("this directive may only occur at module level")
|
|
if(directive.args.size!=1 || directive.args[0].name==null)
|
|
err("invalid import directive, expected module name argument")
|
|
if(directive.args[0].name == (directive.parent as? Module)?.name)
|
|
err("invalid import directive, cannot import itself")
|
|
}
|
|
"%breakpoint" -> {
|
|
if(directive.parent !is INameScope || directive.parent is Module)
|
|
err("this directive can't be used here")
|
|
if(directive.args.isNotEmpty())
|
|
err("invalid breakpoint directive, expected no arguments")
|
|
}
|
|
"%asminclude" -> {
|
|
if(directive.parent !is INameScope || directive.parent is Module)
|
|
err("this directive can't be used here")
|
|
if(directive.args.size!=1 || directive.args[0].str==null)
|
|
err("invalid asminclude directive, expected argument: \"filename\"")
|
|
checkFileExists(directive, directive.args[0].str!!)
|
|
}
|
|
"%asmbinary" -> {
|
|
if(directive.parent !is INameScope || directive.parent is Module)
|
|
err("this directive can't be used here")
|
|
val errormsg = "invalid asmbinary directive, expected arguments: \"filename\" [, offset [, length ] ]"
|
|
if(directive.args.isEmpty()) err(errormsg)
|
|
else if(directive.args[0].str==null) err(errormsg)
|
|
else if(directive.args.size>=2 && directive.args[1].int==null) err(errormsg)
|
|
else if(directive.args.size==3 && directive.args[2].int==null) err(errormsg)
|
|
else if(directive.args.size>3) err(errormsg)
|
|
else checkFileExists(directive, directive.args[0].str!!)
|
|
}
|
|
"%option" -> {
|
|
if(directive.parent !is Block && directive.parent !is Module)
|
|
err("this directive may only occur in a block or at module level")
|
|
if(directive.args.isEmpty())
|
|
err("missing option directive argument(s)")
|
|
else if(directive.args.map{it.name in arrayOf("enable_floats", "force_output", "no_sysinit", "align_word", "align_page")}.any { !it })
|
|
err("invalid option directive argument(s)")
|
|
}
|
|
else -> throw SyntaxError("invalid directive ${directive.directive}", directive.position)
|
|
}
|
|
super.visit(directive)
|
|
}
|
|
|
|
private fun checkFileExists(directive: Directive, filename: String) {
|
|
if (File(filename).isFile)
|
|
return
|
|
|
|
val definingModule = directive.definingModule
|
|
if (definingModule.isLibrary || !definingModule.source.isFromFilesystem)
|
|
return
|
|
|
|
val s = definingModule.source.origin
|
|
val sourceFileCandidate = Path(s).resolveSibling(filename).toFile()
|
|
if (sourceFileCandidate.isFile)
|
|
return
|
|
else
|
|
errors.err("included file not found: $filename", directive.position)
|
|
}
|
|
|
|
override fun visit(array: ArrayLiteral) {
|
|
if(array.type.isKnown) {
|
|
if (!compilerOptions.floats && array.type.oneOf(DataType.FLOAT, DataType.ARRAY_F)) {
|
|
errors.err("floating point used, but that is not enabled via options", array.position)
|
|
}
|
|
val arrayspec = ArrayIndex.forArray(array)
|
|
checkValueTypeAndRangeArray(array.type.getOr(DataType.UNDEFINED), arrayspec, array)
|
|
}
|
|
|
|
fun isPassByReferenceElement(e: Expression): Boolean {
|
|
if(e is IdentifierReference) {
|
|
val decl = e.targetVarDecl(program)!!
|
|
return decl.datatype in PassByReferenceDatatypes
|
|
}
|
|
return e is StringLiteral
|
|
}
|
|
|
|
if(array.parent is VarDecl) {
|
|
if (!array.value.all { it is NumericLiteral || it is AddressOf || isPassByReferenceElement(it) })
|
|
errors.err("array literal for variable initialization contains non-constant elements", array.position)
|
|
} else if(array.parent is ForLoop) {
|
|
if (!array.value.all { it.constValue(program) != null })
|
|
errors.err("array literal for iteration must contain constants. Try using a separate array variable instead?", array.position)
|
|
}
|
|
|
|
super.visit(array)
|
|
}
|
|
|
|
override fun visit(char: CharLiteral) {
|
|
try { // just *try* if it can be encoded, don't actually do it
|
|
compilerOptions.compTarget.encodeString(char.value.toString(), char.encoding)
|
|
} catch (cx: CharConversionException) {
|
|
errors.err(cx.message ?: "can't encode character", char.position)
|
|
}
|
|
|
|
super.visit(char)
|
|
}
|
|
|
|
override fun visit(string: StringLiteral) {
|
|
checkValueTypeAndRangeString(DataType.STR, string)
|
|
|
|
try { // just *try* if it can be encoded, don't actually do it
|
|
val bytes = compilerOptions.compTarget.encodeString(string.value, string.encoding)
|
|
if(0u in bytes)
|
|
errors.warn("a character in the string encodes into the 0-byte, which will terminate the string prematurely", string.position)
|
|
} catch (cx: CharConversionException) {
|
|
errors.err(cx.message ?: "can't encode string", string.position)
|
|
}
|
|
|
|
super.visit(string)
|
|
}
|
|
|
|
override fun visit(expr: PrefixExpression) {
|
|
val dt = expr.expression.inferType(program).getOr(DataType.UNDEFINED)
|
|
if(dt==DataType.UNDEFINED)
|
|
return // any error should be reported elsewhere
|
|
|
|
if(expr.operator=="-") {
|
|
if (dt != DataType.BYTE && dt != DataType.WORD && dt != DataType.FLOAT) {
|
|
errors.err("can only take negative of a signed number type", expr.position)
|
|
}
|
|
}
|
|
else if(expr.operator == "not") {
|
|
if(dt !in IntegerDatatypes)
|
|
errors.err("can only use boolean not on integer types", expr.position)
|
|
}
|
|
else if(expr.operator == "~") {
|
|
if(dt !in IntegerDatatypes)
|
|
errors.err("can only use bitwise invert on integer types", expr.position)
|
|
}
|
|
super.visit(expr)
|
|
}
|
|
|
|
override fun visit(expr: BinaryExpression) {
|
|
super.visit(expr)
|
|
|
|
val leftIDt = expr.left.inferType(program)
|
|
val rightIDt = expr.right.inferType(program)
|
|
if(!leftIDt.isKnown || !rightIDt.isKnown)
|
|
return // hopefully this error will be detected elsewhere
|
|
|
|
val leftDt = leftIDt.getOr(DataType.UNDEFINED)
|
|
val rightDt = rightIDt.getOr(DataType.UNDEFINED)
|
|
|
|
if(expr.operator=="+" || expr.operator=="-") {
|
|
if(leftDt == DataType.STR || rightDt == DataType.STR || leftDt in ArrayDatatypes || rightDt in ArrayDatatypes) {
|
|
errors.err("missing & (address-of) on the string operand", expr.position)
|
|
return
|
|
}
|
|
}
|
|
|
|
when(expr.operator){
|
|
"/", "%" -> {
|
|
val constvalRight = expr.right.constValue(program)
|
|
val divisor = constvalRight?.number
|
|
if(divisor==0.0)
|
|
errors.err("division by zero", expr.right.position)
|
|
if(expr.operator=="%") {
|
|
if ((rightDt != DataType.UBYTE && rightDt != DataType.UWORD) || (leftDt!= DataType.UBYTE && leftDt!= DataType.UWORD))
|
|
errors.err("remainder can only be used on unsigned integer operands", expr.right.position)
|
|
}
|
|
}
|
|
"**" -> {
|
|
if(leftDt in IntegerDatatypes)
|
|
errors.err("power operator requires floating point operands", expr.position)
|
|
}
|
|
"and", "or", "xor" -> {
|
|
// only integer numeric operands accepted, and if literal constants, only boolean values accepted (0 or 1)
|
|
if(leftDt !in IntegerDatatypes || rightDt !in IntegerDatatypes)
|
|
errors.err("logical operator can only be used on boolean operands", expr.right.position)
|
|
val constLeft = expr.left.constValue(program)
|
|
val constRight = expr.right.constValue(program)
|
|
if(constLeft!=null && constLeft.number.toInt() !in 0..1 || constRight!=null && constRight.number.toInt() !in 0..1)
|
|
errors.err("const literal argument of logical operator must be boolean (0 or 1)", expr.position)
|
|
}
|
|
"&", "|", "^" -> {
|
|
// only integer numeric operands accepted
|
|
if(leftDt !in IntegerDatatypes || rightDt !in IntegerDatatypes)
|
|
errors.err("bitwise operator can only be used on integer operands", expr.right.position)
|
|
}
|
|
"in" -> throw FatalAstException("in expression should have been replaced by containmentcheck")
|
|
}
|
|
|
|
if(leftDt !in NumericDatatypes && leftDt != DataType.STR)
|
|
errors.err("left operand is not numeric or str", expr.left.position)
|
|
if(rightDt!in NumericDatatypes && rightDt != DataType.STR)
|
|
errors.err("right operand is not numeric or str", expr.right.position)
|
|
if(leftDt!=rightDt) {
|
|
if(leftDt==DataType.STR && rightDt in IntegerDatatypes) {
|
|
// only exception allowed: str * constvalue
|
|
if(expr.right.constValue(program)!=null)
|
|
errors.err("can only use string repeat with a constant number value", expr.left.position)
|
|
} else {
|
|
errors.err("left and right operands aren't the same type", expr.left.position)
|
|
}
|
|
}
|
|
|
|
if(expr.operator !in ComparisonOperators) {
|
|
if (leftDt == DataType.STR && rightDt == DataType.STR || leftDt in ArrayDatatypes && rightDt in ArrayDatatypes) {
|
|
// str+str and str*number have already been const evaluated before we get here.
|
|
errors.err("no computational expressions with strings or arrays are possible", expr.position)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
override fun visit(typecast: TypecastExpression) {
|
|
if(typecast.type in IterableDatatypes)
|
|
errors.err("cannot type cast to string or array type", typecast.position)
|
|
|
|
if(!typecast.expression.inferType(program).isKnown)
|
|
errors.err("this expression doesn't return a value", typecast.expression.position)
|
|
|
|
if(typecast.expression is NumericLiteral)
|
|
errors.err("can't cast the value to the requested target type", typecast.expression.position)
|
|
|
|
super.visit(typecast)
|
|
}
|
|
|
|
override fun visit(range: RangeExpression) {
|
|
fun err(msg: String) {
|
|
errors.err(msg, range.position)
|
|
}
|
|
super.visit(range)
|
|
val from = range.from.constValue(program)
|
|
val to = range.to.constValue(program)
|
|
val stepLv = range.step.constValue(program)
|
|
if(stepLv==null) {
|
|
err("range step must be a constant integer")
|
|
return
|
|
} else if (stepLv.type !in IntegerDatatypes || stepLv.number.toInt() == 0) {
|
|
err("range step must be an integer != 0")
|
|
return
|
|
}
|
|
val step = stepLv.number.toInt()
|
|
if(from!=null && to != null) {
|
|
when {
|
|
from.type in IntegerDatatypes && to.type in IntegerDatatypes -> {
|
|
val fromValue = from.number.toInt()
|
|
val toValue = to.number.toInt()
|
|
if(fromValue== toValue)
|
|
errors.warn("range is just a single value, don't use a loop here", range.position)
|
|
else if(fromValue < toValue && step<=0)
|
|
err("ascending range requires step > 0")
|
|
else if(fromValue > toValue && step>=0)
|
|
err("descending range requires step < 0")
|
|
}
|
|
else -> err("range expression must be over integers or over characters")
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun visit(functionCallExpr: FunctionCallExpression) {
|
|
// this function call is (part of) an expression, which should be in a statement somewhere.
|
|
val stmtOfExpression = findParentNode<Statement>(functionCallExpr)
|
|
?: throw FatalAstException("cannot determine statement scope of function call expression at ${functionCallExpr.position}")
|
|
|
|
val targetStatement = checkFunctionOrLabelExists(functionCallExpr.target, stmtOfExpression)
|
|
if(targetStatement!=null)
|
|
checkFunctionCall(targetStatement, functionCallExpr.args, functionCallExpr.position)
|
|
|
|
// warn about sgn(unsigned) this is likely a mistake
|
|
if(functionCallExpr.target.nameInSource.last()=="sgn") {
|
|
val sgnArgType = functionCallExpr.args.first().inferType(program)
|
|
if(sgnArgType istype DataType.UBYTE || sgnArgType istype DataType.UWORD)
|
|
errors.warn("sgn() of unsigned type is always 0 or 1, this is perhaps not what was intended", functionCallExpr.args.first().position)
|
|
}
|
|
|
|
val error = VerifyFunctionArgTypes.checkTypes(functionCallExpr, program)
|
|
if(error!=null)
|
|
errors.err(error.first, error.second)
|
|
|
|
// check the functions that return multiple returnvalues.
|
|
val stmt = functionCallExpr.target.targetStatement(program)
|
|
if (stmt is Subroutine) {
|
|
if (stmt.returntypes.size > 1) {
|
|
// Currently, it's only possible to handle ONE (or zero) return values from a subroutine.
|
|
// asmsub routines can have multiple return values, for instance in 2 different registers.
|
|
// It's not (yet) possible to handle these multiple return values because assignments
|
|
// are only to a single unique target at the same time.
|
|
// EXCEPTION:
|
|
// if the asmsub returns multiple values and one of them is via a status register bit,
|
|
// it *is* possible to handle them by just actually assigning the register value and
|
|
// dealing with the status bit as just being that, the status bit after the call.
|
|
val (returnRegisters, _) = stmt.asmReturnvaluesRegisters.partition { rr -> rr.registerOrPair != null }
|
|
if (returnRegisters.size>1) {
|
|
errors.err("It's not possible to store the multiple result values of this asmsub call; you should use a small block of custom inline assembly for this.", functionCallExpr.position)
|
|
}
|
|
}
|
|
}
|
|
|
|
// functions that don't return a value, can't be used in an expression or assignment
|
|
if(targetStatement is Subroutine) {
|
|
if(targetStatement.returntypes.isEmpty()) {
|
|
if(functionCallExpr.parent is Expression || functionCallExpr.parent is Assignment)
|
|
errors.err("subroutine doesn't return a value", functionCallExpr.position)
|
|
}
|
|
}
|
|
else if(targetStatement is BuiltinFunctionPlaceholder) {
|
|
val args = if(functionCallExpr.parent is IPipe) {
|
|
// pipe segment, add implicit first argument
|
|
val firstArgDt = BuiltinFunctions.getValue(targetStatement.name).parameters.first().possibleDatatypes.first()
|
|
listOf(defaultZero(firstArgDt, functionCallExpr.position)) + functionCallExpr.args
|
|
} else {
|
|
functionCallExpr.args
|
|
}
|
|
if(builtinFunctionReturnType(targetStatement.name, args, program).isUnknown) {
|
|
if(functionCallExpr.parent is Expression || functionCallExpr.parent is Assignment)
|
|
errors.err("function doesn't return a value", functionCallExpr.position)
|
|
}
|
|
}
|
|
|
|
super.visit(functionCallExpr)
|
|
}
|
|
|
|
override fun visit(functionCallStatement: FunctionCallStatement) {
|
|
val targetStatement = checkFunctionOrLabelExists(functionCallStatement.target, functionCallStatement)
|
|
if(targetStatement!=null) {
|
|
checkFunctionCall(targetStatement, functionCallStatement.args, functionCallStatement.position)
|
|
checkUnusedReturnValues(functionCallStatement, targetStatement, program, errors)
|
|
}
|
|
|
|
val funcName = functionCallStatement.target.nameInSource
|
|
|
|
if(funcName.size==1) {
|
|
// check some builtin function calls
|
|
if(funcName[0] == "sort") {
|
|
// sort is not supported on float arrays
|
|
val idref = functionCallStatement.args.singleOrNull() as? IdentifierReference
|
|
if(idref!=null && idref.inferType(program) istype DataType.ARRAY_F) {
|
|
errors.err("sorting a floating point array is not supported", functionCallStatement.args.first().position)
|
|
}
|
|
}
|
|
else if(funcName[0] in arrayOf("pop", "popw")) {
|
|
// can only pop into a variable, that has to have the correct type
|
|
val idref = functionCallStatement.args[0]
|
|
if(idref !is IdentifierReference) {
|
|
if(idref is TypecastExpression) {
|
|
val passByRef = idref.expression.inferType(program).isPassByReference
|
|
if(idref.type!=DataType.UWORD || !passByRef)
|
|
errors.err("invalid argument to pop, must be a variable with the correct type: ${functionCallStatement.args.first()}", functionCallStatement.args.first().position)
|
|
} else {
|
|
errors.err("invalid argument to pop, must be a variable with the correct type: ${functionCallStatement.args.first()}", functionCallStatement.args.first().position)
|
|
}
|
|
}
|
|
}
|
|
|
|
if(funcName[0] in arrayOf("rol", "ror", "rol2", "ror2", "swap", "sort", "reverse")) {
|
|
// in-place modification, can't be done on literals
|
|
if(functionCallStatement.args.any { it !is IdentifierReference && it !is ArrayIndexedExpression && it !is DirectMemoryRead }) {
|
|
errors.err("invalid argument to a in-place modifying function", functionCallStatement.args.first().position)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
val error = VerifyFunctionArgTypes.checkTypes(functionCallStatement, program)
|
|
if(error!=null)
|
|
errors.err(error.first, error.second)
|
|
|
|
super.visit(functionCallStatement)
|
|
}
|
|
|
|
private fun checkFunctionCall(target: Statement, args: List<Expression>, position: Position) {
|
|
if(target is Label && args.isNotEmpty())
|
|
errors.err("cannot use arguments when calling a label", position)
|
|
|
|
if(target is BuiltinFunctionPlaceholder) {
|
|
if(target.name=="swap") {
|
|
// swap() is a bit weird because this one is translated into an operations directly, instead of being a function call
|
|
val dt1 = args[0].inferType(program)
|
|
val dt2 = args[1].inferType(program)
|
|
if (dt1 != dt2)
|
|
errors.err("swap requires 2 args of identical type", position)
|
|
else if (args[0].constValue(program) != null || args[1].constValue(program) != null)
|
|
errors.err("swap requires 2 variables, not constant value(s)", position)
|
|
else if(args[0] isSameAs args[1])
|
|
errors.err("swap should have 2 different args", position)
|
|
else if(!dt1.isNumeric)
|
|
errors.err("swap requires args of numerical type", position)
|
|
}
|
|
else if(target.name=="all" || target.name=="any") {
|
|
if((args[0] as? AddressOf)?.identifier?.targetVarDecl(program)?.datatype == DataType.STR) {
|
|
errors.err("any/all on a string is useless (is always true unless the string is empty)", position)
|
|
}
|
|
if(args[0].inferType(program).getOr(DataType.STR) == DataType.STR) {
|
|
errors.err("any/all on a string is useless (is always true unless the string is empty)", position)
|
|
}
|
|
}
|
|
} else if(target is Subroutine) {
|
|
if(target.isAsmSubroutine) {
|
|
for (arg in args.zip(target.parameters)) {
|
|
val argIDt = arg.first.inferType(program)
|
|
if (!argIDt.isKnown)
|
|
return
|
|
}
|
|
|
|
// check that cx16 virtual registers aren't used as arguments in a conflicting way
|
|
val params = target.asmParameterRegisters.withIndex().toList()
|
|
for(arg in args.withIndex()) {
|
|
var ident: IdentifierReference? = null
|
|
if(arg.value is IdentifierReference)
|
|
ident = arg.value as IdentifierReference
|
|
else if(arg.value is FunctionCallExpression) {
|
|
val fcall = arg.value as FunctionCallExpression
|
|
if(fcall.target.nameInSource == listOf("lsb") || fcall.target.nameInSource == listOf("msb"))
|
|
ident = fcall.args[0] as? IdentifierReference
|
|
}
|
|
if(ident!=null && ident.nameInSource[0] == "cx16" && ident.nameInSource[1].startsWith("r")) {
|
|
var regname = ident.nameInSource[1].uppercase()
|
|
if(regname.endsWith('L') || regname.endsWith('H') || regname.endsWith('s'))
|
|
regname=regname.substring(0, regname.length-1)
|
|
val reg = RegisterOrPair.valueOf(regname)
|
|
val same = params.filter { it.value.registerOrPair==reg }
|
|
for(s in same) {
|
|
if(s.index!=arg.index) {
|
|
errors.err("conflicting register $reg used as argument but is also a target register for another parameter", ident.position)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun visit(pipe: PipeExpression) = process(pipe)
|
|
|
|
override fun visit(pipe: Pipe) = process(pipe)
|
|
|
|
private fun process(pipe: IPipe) {
|
|
if(pipe.source in pipe.segments)
|
|
throw InternalCompilerException("pipe source and segments should all be different nodes")
|
|
if (pipe.segments.isEmpty())
|
|
throw FatalAstException("pipe is missing one or more expressions")
|
|
if(pipe.segments.any { it !is IFunctionCall })
|
|
throw FatalAstException("pipe segments can only be function calls")
|
|
}
|
|
|
|
override fun visit(postIncrDecr: PostIncrDecr) {
|
|
if(postIncrDecr.target.identifier != null) {
|
|
val targetName = postIncrDecr.target.identifier!!.nameInSource
|
|
val target = postIncrDecr.definingScope.lookup(targetName)
|
|
if(target==null) {
|
|
val symbol = postIncrDecr.target.identifier!!
|
|
errors.err("undefined symbol: ${symbol.nameInSource.joinToString(".")}", symbol.position)
|
|
} else {
|
|
if(target !is VarDecl || target.type== VarDeclType.CONST) {
|
|
errors.err("can only increment or decrement a variable", postIncrDecr.position)
|
|
} else if(target.datatype !in NumericDatatypes) {
|
|
errors.err("can only increment or decrement a byte/float/word variable", postIncrDecr.position)
|
|
}
|
|
}
|
|
} else if(postIncrDecr.target.arrayindexed != null) {
|
|
val target = postIncrDecr.target.arrayindexed?.arrayvar?.targetStatement(program)
|
|
if(target==null) {
|
|
errors.err("undefined symbol", postIncrDecr.position)
|
|
}
|
|
else {
|
|
val dt = (target as VarDecl).datatype
|
|
if(dt !in NumericDatatypes && dt !in ArrayDatatypes)
|
|
errors.err("can only increment or decrement a byte/float/word", postIncrDecr.position)
|
|
}
|
|
}
|
|
// else if(postIncrDecr.target.memoryAddress != null) { } // a memory location can always be ++/--
|
|
super.visit(postIncrDecr)
|
|
}
|
|
|
|
override fun visit(arrayIndexedExpression: ArrayIndexedExpression) {
|
|
val target = arrayIndexedExpression.arrayvar.targetStatement(program)
|
|
if(target is VarDecl) {
|
|
if(target.datatype !in IterableDatatypes)
|
|
errors.err("indexing requires an iterable variable", arrayIndexedExpression.position)
|
|
val arraysize = target.arraysize?.constIndex()
|
|
if(arraysize!=null) {
|
|
// check out of bounds
|
|
val index = arrayIndexedExpression.indexer.constIndex()
|
|
if(index!=null && (index<0 || index>=arraysize))
|
|
errors.err("array index out of bounds", arrayIndexedExpression.indexer.position)
|
|
} else if(target.datatype == DataType.STR) {
|
|
if(target.value is StringLiteral) {
|
|
// check string lengths for non-memory mapped strings
|
|
val stringLen = (target.value as StringLiteral).value.length
|
|
val index = arrayIndexedExpression.indexer.constIndex()
|
|
if (index != null && (index < 0 || index >= stringLen))
|
|
errors.err("index out of bounds", arrayIndexedExpression.indexer.position)
|
|
}
|
|
}
|
|
} else
|
|
errors.err("indexing requires a variable to act upon", arrayIndexedExpression.position)
|
|
|
|
// check index value 0..255
|
|
val dtxNum = arrayIndexedExpression.indexer.indexExpr.inferType(program)
|
|
if(dtxNum isnot DataType.UBYTE && dtxNum isnot DataType.BYTE)
|
|
errors.err("array indexing is limited to byte size 0..255", arrayIndexedExpression.position)
|
|
|
|
super.visit(arrayIndexedExpression)
|
|
}
|
|
|
|
override fun visit(whenStmt: When) {
|
|
if(!whenStmt.condition.inferType(program).isInteger)
|
|
errors.err("when condition must be an integer value", whenStmt.position)
|
|
val tally = mutableSetOf<Int>()
|
|
for((choices, choiceNode) in whenStmt.choiceValues(program)) {
|
|
if(choices!=null) {
|
|
for (c in choices) {
|
|
if(c in tally)
|
|
errors.err("choice value already occurs earlier", choiceNode.position)
|
|
else
|
|
tally.add(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
if(whenStmt.choices.isEmpty())
|
|
errors.err("empty when statement", whenStmt.position)
|
|
|
|
super.visit(whenStmt)
|
|
}
|
|
|
|
override fun visit(whenChoice: WhenChoice) {
|
|
val whenStmt = whenChoice.parent as When
|
|
if(whenChoice.values!=null) {
|
|
val conditionType = whenStmt.condition.inferType(program)
|
|
if(!conditionType.isKnown)
|
|
throw FatalAstException("can't determine when choice datatype $whenChoice")
|
|
val constvalues = whenChoice.values!!.map { it.constValue(program) }
|
|
for(constvalue in constvalues) {
|
|
when {
|
|
constvalue == null -> errors.err("choice value must be a constant", whenChoice.position)
|
|
constvalue.type !in IntegerDatatypes -> errors.err("choice value must be a byte or word", whenChoice.position)
|
|
conditionType isnot constvalue.type -> errors.err("choice value datatype differs from condition value", whenChoice.position)
|
|
}
|
|
}
|
|
} else {
|
|
if(whenChoice !== whenStmt.choices.last())
|
|
errors.err("else choice must be the last one", whenChoice.position)
|
|
}
|
|
super.visit(whenChoice)
|
|
}
|
|
|
|
override fun visit(containment: ContainmentCheck) {
|
|
val elementDt = containment.element.inferType(program)
|
|
val iterableDt = containment.iterable.inferType(program)
|
|
|
|
if(containment.parent is BinaryExpression)
|
|
errors.err("containment check is currently not supported in complex expressions", containment.position)
|
|
|
|
val range = containment.iterable as? RangeExpression
|
|
if(range!=null && range.toConstantIntegerRange()==null)
|
|
errors.err("containment check requires a constant integer range", range.position)
|
|
|
|
if(iterableDt.isIterable) {
|
|
val iterableEltDt = ArrayToElementTypes.getValue(iterableDt.getOr(DataType.UNDEFINED))
|
|
val invalidDt = if (elementDt.isBytes) {
|
|
iterableEltDt !in ByteDatatypes
|
|
} else if (elementDt.isWords) {
|
|
iterableEltDt !in WordDatatypes
|
|
} else {
|
|
false
|
|
}
|
|
if (invalidDt)
|
|
errors.err("element datatype doesn't match iterable datatype", containment.position)
|
|
} else {
|
|
errors.err("value set for containment check must be an iterable type", containment.iterable.position)
|
|
}
|
|
|
|
super.visit(containment)
|
|
}
|
|
|
|
private fun checkFunctionOrLabelExists(target: IdentifierReference, statement: Statement): Statement? {
|
|
when (val targetStatement = target.targetStatement(program)) {
|
|
is Label, is Subroutine, is BuiltinFunctionPlaceholder -> return targetStatement
|
|
is VarDecl -> {
|
|
if(statement is Jump) {
|
|
if (targetStatement.datatype == DataType.UWORD)
|
|
return targetStatement
|
|
else
|
|
errors.err("wrong address variable datatype, expected uword", target.position)
|
|
}
|
|
else
|
|
errors.err("cannot call that: ${target.nameInSource.joinToString(".")}", target.position)
|
|
}
|
|
null -> errors.err("undefined function or subroutine: ${target.nameInSource.joinToString(".")}", target.position)
|
|
else -> errors.err("cannot call that: ${target.nameInSource.joinToString(".")}", target.position)
|
|
}
|
|
return null
|
|
}
|
|
|
|
private fun checkValueTypeAndRangeString(targetDt: DataType, value: StringLiteral) : Boolean {
|
|
return if (targetDt == DataType.STR) {
|
|
when {
|
|
value.value.length > 255 -> {
|
|
errors.err("string length must be 0-255", value.position)
|
|
false
|
|
}
|
|
value.value.isEmpty() -> {
|
|
val decl = value.parent as? VarDecl
|
|
if(decl!=null && (decl.zeropage==ZeropageWish.REQUIRE_ZEROPAGE || decl.zeropage==ZeropageWish.PREFER_ZEROPAGE)) {
|
|
errors.err("string in Zeropage must be non-empty", value.position)
|
|
false
|
|
}
|
|
else true
|
|
}
|
|
else -> true
|
|
}
|
|
}
|
|
else false
|
|
}
|
|
|
|
private fun checkValueTypeAndRangeArray(targetDt: DataType, arrayspec: ArrayIndex, value: ArrayLiteral) : Boolean {
|
|
fun err(msg: String) : Boolean {
|
|
errors.err(msg, value.position)
|
|
return false
|
|
}
|
|
|
|
if(value.type.isUnknown)
|
|
return false
|
|
|
|
when (targetDt) {
|
|
DataType.STR -> return err("string value expected")
|
|
DataType.ARRAY_UB, DataType.ARRAY_B -> {
|
|
// value may be either a single byte, or a byte arraysize (of all constant values), or a range
|
|
if(value.type istype targetDt) {
|
|
if(!checkArrayValues(value, targetDt))
|
|
return false
|
|
val arraySpecSize = arrayspec.constIndex()
|
|
val arraySize = value.value.size
|
|
if(arraySpecSize!=null && arraySpecSize>0) {
|
|
if(arraySpecSize>256)
|
|
return err("byte array length must be 1-256")
|
|
val expectedSize = arrayspec.constIndex() ?: return err("array size specifier must be constant integer value")
|
|
if (arraySize != expectedSize)
|
|
return err("initializer array size mismatch (expecting $expectedSize, got $arraySize)")
|
|
return true
|
|
}
|
|
return err("invalid byte array size, must be 1-256")
|
|
}
|
|
return err("invalid byte array initialization value ${value.type}, expected $targetDt")
|
|
}
|
|
DataType.ARRAY_UW, DataType.ARRAY_W -> {
|
|
// value may be either a single word, or a word arraysize, or a range
|
|
if(value.type istype targetDt) {
|
|
if(!checkArrayValues(value, targetDt))
|
|
return false
|
|
val arraySpecSize = arrayspec.constIndex()
|
|
val arraySize = value.value.size
|
|
if(arraySpecSize!=null && arraySpecSize>0) {
|
|
if(arraySpecSize>128)
|
|
return err("word array length must be 1-128")
|
|
val expectedSize = arrayspec.constIndex() ?: return err("array size specifier must be constant integer value")
|
|
if (arraySize != expectedSize)
|
|
return err("initializer array size mismatch (expecting $expectedSize, got $arraySize)")
|
|
return true
|
|
}
|
|
return err("invalid word array size, must be 1-128")
|
|
}
|
|
return err("invalid word array initialization value ${value.type}, expected $targetDt")
|
|
}
|
|
DataType.ARRAY_F -> {
|
|
// value may be either a single float, or a float arraysize
|
|
if(value.type istype targetDt) {
|
|
if(!checkArrayValues(value, targetDt))
|
|
return false
|
|
val arraySize = value.value.size
|
|
val arraySpecSize = arrayspec.constIndex()
|
|
if(arraySpecSize!=null && arraySpecSize>0) {
|
|
if(arraySpecSize>51)
|
|
return err("float array length must be 1-51")
|
|
val expectedSize = arrayspec.constIndex() ?: return err("array size specifier must be constant integer value")
|
|
if (arraySize != expectedSize)
|
|
return err("initializer array size mismatch (expecting $expectedSize, got $arraySize)")
|
|
} else
|
|
return err("invalid float array size, must be 1-51")
|
|
|
|
// check if the floating point values are all within range
|
|
val doubles = value.value.map {it.constValue(program)?.number!!.toDouble()}.toDoubleArray()
|
|
if(doubles.any { it < compilerOptions.compTarget.machine.FLOAT_MAX_NEGATIVE || it > compilerOptions.compTarget.machine.FLOAT_MAX_POSITIVE })
|
|
return err("floating point value overflow")
|
|
return true
|
|
}
|
|
return err("invalid float array initialization value ${value.type}, expected $targetDt")
|
|
}
|
|
else -> return false
|
|
}
|
|
}
|
|
|
|
private fun checkValueTypeAndRange(targetDt: DataType, value: NumericLiteral) : Boolean {
|
|
fun err(msg: String) : Boolean {
|
|
errors.err(msg, value.position)
|
|
return false
|
|
}
|
|
when (targetDt) {
|
|
DataType.FLOAT -> {
|
|
val number=value.number
|
|
if (number > 1.7014118345e+38 || number < -1.7014118345e+38)
|
|
return err("value '$number' out of range for MFLPT format")
|
|
}
|
|
DataType.UBYTE -> {
|
|
if(value.type==DataType.FLOAT)
|
|
err("unsigned byte value expected instead of float; possible loss of precision")
|
|
val number=value.number.toInt()
|
|
if (number < 0 || number > 255)
|
|
return err("value '$number' out of range for unsigned byte")
|
|
}
|
|
DataType.BYTE -> {
|
|
if(value.type==DataType.FLOAT)
|
|
err("byte value expected instead of float; possible loss of precision")
|
|
val number=value.number.toInt()
|
|
if (number < -128 || number > 127)
|
|
return err("value '$number' out of range for byte")
|
|
}
|
|
DataType.UWORD -> {
|
|
if(value.type==DataType.FLOAT)
|
|
err("unsigned word value expected instead of float; possible loss of precision")
|
|
val number=value.number.toInt()
|
|
if (number < 0 || number > 65535)
|
|
return err("value '$number' out of range for unsigned word")
|
|
}
|
|
DataType.WORD -> {
|
|
if(value.type==DataType.FLOAT)
|
|
err("word value expected instead of float; possible loss of precision")
|
|
val number=value.number.toInt()
|
|
if (number < -32768 || number > 32767)
|
|
return err("value '$number' out of range for word")
|
|
}
|
|
else -> return err("value of type ${value.type} not compatible with $targetDt")
|
|
}
|
|
return true
|
|
}
|
|
|
|
private fun checkArrayValues(value: ArrayLiteral, type: DataType): Boolean {
|
|
val array = value.value.map {
|
|
when (it) {
|
|
is NumericLiteral -> it.number.toInt()
|
|
is AddressOf -> it.identifier.hashCode() and 0xffff
|
|
is TypecastExpression -> {
|
|
val constVal = it.expression.constValue(program)
|
|
val cast = constVal?.cast(it.type)
|
|
if(cast==null || !cast.isValid)
|
|
-9999999
|
|
else
|
|
cast.valueOrZero().number.toInt()
|
|
}
|
|
else -> -9999999
|
|
}
|
|
}
|
|
val correct: Boolean
|
|
when (type) {
|
|
DataType.ARRAY_UB -> {
|
|
correct = array.all { it in 0..255 }
|
|
}
|
|
DataType.ARRAY_B -> {
|
|
correct = array.all { it in -128..127 }
|
|
}
|
|
DataType.ARRAY_UW -> {
|
|
correct = array.all { (it in 0..65535) }
|
|
}
|
|
DataType.ARRAY_W -> {
|
|
correct = array.all { it in -32768..32767 }
|
|
}
|
|
DataType.ARRAY_F -> correct = true
|
|
else -> throw FatalAstException("invalid array type $type")
|
|
}
|
|
if (!correct)
|
|
errors.err("array value out of range for type $type", value.position)
|
|
return correct
|
|
}
|
|
|
|
private fun checkAssignmentCompatible(targetDatatype: DataType,
|
|
sourceDatatype: DataType,
|
|
sourceValue: Expression) : Boolean {
|
|
val position = sourceValue.position
|
|
|
|
if(sourceValue is RangeExpression)
|
|
errors.err("can't assign a range value to something else", position)
|
|
|
|
val result = when(targetDatatype) {
|
|
DataType.BYTE -> sourceDatatype== DataType.BYTE
|
|
DataType.UBYTE -> sourceDatatype== DataType.UBYTE
|
|
DataType.WORD -> sourceDatatype== DataType.BYTE || sourceDatatype== DataType.UBYTE || sourceDatatype== DataType.WORD
|
|
DataType.UWORD -> sourceDatatype== DataType.UBYTE || sourceDatatype== DataType.UWORD
|
|
DataType.FLOAT -> sourceDatatype in NumericDatatypes
|
|
DataType.STR -> sourceDatatype== DataType.STR
|
|
else -> {
|
|
errors.err("cannot assign new value to variable of type $targetDatatype", position)
|
|
false
|
|
}
|
|
}
|
|
|
|
if(result)
|
|
return true
|
|
|
|
if((sourceDatatype== DataType.UWORD || sourceDatatype== DataType.WORD) && (targetDatatype== DataType.UBYTE || targetDatatype== DataType.BYTE)) {
|
|
errors.err("cannot assign word to byte, use msb() or lsb()?", position)
|
|
}
|
|
else if(sourceDatatype== DataType.FLOAT && targetDatatype in IntegerDatatypes)
|
|
errors.err("cannot assign float to ${targetDatatype.name.lowercase()}; possible loss of precision. Suggestion: round the value or revert to integer arithmetic", position)
|
|
else {
|
|
if(targetDatatype!=DataType.UWORD && sourceDatatype !in PassByReferenceDatatypes)
|
|
errors.err("type of value $sourceDatatype doesn't match target $targetDatatype", position)
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
internal fun checkUnusedReturnValues(call: FunctionCallStatement, target: Statement, program: Program, errors: IErrorReporter) {
|
|
if (!call.void) {
|
|
// check for unused return values
|
|
if (target is Subroutine && target.returntypes.isNotEmpty()) {
|
|
if (target.returntypes.size == 1)
|
|
errors.warn("result value of subroutine call is discarded (use void?)", call.position)
|
|
else
|
|
errors.warn("result values of subroutine call are discarded (use void?)", call.position)
|
|
} else if (target is BuiltinFunctionPlaceholder) {
|
|
val rt = builtinFunctionReturnType(target.name, call.args, program)
|
|
if (rt.isKnown)
|
|
errors.warn("result value of a function call is discarded (use void?)", call.position)
|
|
}
|
|
}
|
|
}
|