From ba81f32080ee68094f3b0d39cbf44a68312095f9 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Fri, 14 Sep 2018 19:38:22 +0200 Subject: [PATCH] expression datatype checks improvements --- il65/examples/imported2.ill | 2 +- il65/examples/test.ill | 28 ++-- il65/examples/todos.ill | 29 ----- il65/src/il65/ast/AST.kt | 134 +++++++++++++++++++- il65/src/il65/ast/AstChecker.kt | 34 +++-- il65/src/il65/functions/BuiltinFunctions.kt | 16 +-- 6 files changed, 172 insertions(+), 71 deletions(-) delete mode 100644 il65/examples/todos.ill diff --git a/il65/examples/imported2.ill b/il65/examples/imported2.ill index feb94dff6..cf4b0e3af 100644 --- a/il65/examples/imported2.ill +++ b/il65/examples/imported2.ill @@ -24,7 +24,7 @@ return } - sub thingy()->() { + sub thingy()->(X) { ;return 99 return } diff --git a/il65/examples/test.ill b/il65/examples/test.ill index bf995b682..67fc94d21 100644 --- a/il65/examples/test.ill +++ b/il65/examples/test.ill @@ -101,17 +101,26 @@ byte equalQQ = 4==4 const byte equalQQ2 = (4+hopla)>0 + const str string1 = "hallo" + str string2 = "doei" equalQQ++ AX++ - A=X + round(sin(Y)) + A=msb(X + round(sin(Y)+ 1111)) + A=lsb(X + round(sin(Y)+1111)) equalQQ= X equalQQ= len([X, Y, AX]) + equalQQ= len("abcdef") + equalQQ= len([1,2,3]) + equalQQ= len(string1) + equalQQ= len(string2) ; @todo len function call on str type ALSO ADD ALL THIS TO DOCS OF FUNCTION LEN! P_carry(1) P_irqd(0) - equalQQ = foo(33) - equalQQ = main.foo(33) + ;equalQQ = foo(33) + ;equalQQ = main.foo(33) + foo(33) + main.foo(33) XY = hopla*2+hopla1 byte equalWW = 4==4 @@ -130,11 +139,11 @@ } if(6==6) { - A=sin(X) - X=max([1,2,Y]) - X=min([1,2,Y]) + A=sin(X) ; @todo should give error of float loss of precision + X=max([1,2,Y]) ; @todo must be byte so should be ok + X=min([1,2,Y]) ; @todo must be byte so should be ok X=lsl(12) - X=lsl(Y) + X=lsl(Y) ; @todo must be byte so should be ok P_carry(0) P_carry(1) P_carry(1-1) @@ -159,10 +168,8 @@ word blerp1 =999 word blerp3 = 1 byte blerp2 =99 + float flob =1.1 dinges=blerp1 - A=blerp1 ; @todo error can't assign word to byte - A=blerp3 ; @todo error can't assign word to byte - blerp2=blerp3; @todo error can't assign word to byte blerp3 = blerp2 A=blerp2 A=$02 @@ -172,7 +179,6 @@ A=%0010011001 A=99.w XY=blerp1 - X=blerp1 ; @todo error can't assign word to byte X=blerp2 return } diff --git a/il65/examples/todos.ill b/il65/examples/todos.ill deleted file mode 100644 index f5315d1c7..000000000 --- a/il65/examples/todos.ill +++ /dev/null @@ -1,29 +0,0 @@ -%output prg -%launcher basic - -~ main { -; memory byte derp = max([$ffdd]) ; @todo implement memory vars in stackvm -; memory byte derpA = abs(-20000) -; memory byte derpB = max([1, 2.2, 4.4, 100]) -; memory byte cderp = min([$ffdd])+ (1/1) -; memory byte cderpA = min([$ffdd, 10, 20, 30]) -; memory byte cderpB = min([1, 2.2, 4.4, 100]) -; memory byte derp2 = 2+$ffdd+round(10*sin(3.1)) - -sub start() -> () { - P_irqd(1) ; is okay. (has side-effects, is not a pure function) - - word dinges = 0 - word blerp1 =999 - word blerp3 = 1 - byte blerp2 =99 - dinges=blerp1 - blerp3 = blerp2 - A=blerp2 - A=X - XY=X - XY=AX - - return - } -} diff --git a/il65/src/il65/ast/AST.kt b/il65/src/il65/ast/AST.kt index a347620f5..f5218924f 100644 --- a/il65/src/il65/ast/AST.kt +++ b/il65/src/il65/ast/AST.kt @@ -5,6 +5,7 @@ import il65.parser.il65Parser import org.antlr.v4.runtime.ParserRuleContext import org.antlr.v4.runtime.tree.TerminalNode import java.nio.file.Paths +import javax.xml.crypto.Data import kotlin.math.floor @@ -630,7 +631,7 @@ interface IExpression: Node { fun constValue(namespace: INameScope): LiteralValue? fun process(processor: IAstProcessor): IExpression fun referencesIdentifier(name: String): Boolean - // TODO fun resultingDatatype(): DataType + fun resultingDatatype(namespace: INameScope): DataType? } @@ -648,6 +649,7 @@ class PrefixExpression(val operator: String, var expression: IExpression) : IExp override fun constValue(namespace: INameScope): LiteralValue? = null override fun process(processor: IAstProcessor) = processor.process(this) override fun referencesIdentifier(name: String) = expression.referencesIdentifier(name) + override fun resultingDatatype(namespace: INameScope): DataType? = expression.resultingDatatype(namespace) } @@ -666,6 +668,45 @@ class BinaryExpression(var left: IExpression, val operator: String, var right: I override fun process(processor: IAstProcessor) = processor.process(this) override fun referencesIdentifier(name: String) = left.referencesIdentifier(name) || right.referencesIdentifier(name) + override fun resultingDatatype(namespace: INameScope): DataType? { + val leftDt = left.resultingDatatype(namespace) + val rightDt = right.resultingDatatype(namespace) + return when(operator) { + "+", "-", "*", "/", "**" -> if(leftDt==null || rightDt==null) null else arithmeticOpDt(leftDt, rightDt) + "&" -> leftDt + "|" -> leftDt + "^" -> leftDt + "and", "or", "xor", + "<", ">", + "<=", ">=", + "==", "!=" -> DataType.BYTE + else -> throw FatalAstException("resulting datatype check for invalid operator $operator") + } + } + + private fun arithmeticOpDt(leftDt: DataType, rightDt: DataType): DataType { + return when(leftDt) { + DataType.BYTE -> when(rightDt) { + DataType.BYTE -> DataType.BYTE + DataType.WORD -> DataType.WORD + DataType.FLOAT -> DataType.FLOAT + else -> throw FatalAstException("arithmetic operation on incompatible datatypes: $leftDt and $rightDt") + } + DataType.WORD -> when(rightDt) { + DataType.BYTE -> DataType.BYTE + DataType.WORD -> DataType.WORD + DataType.FLOAT -> DataType.FLOAT + else -> throw FatalAstException("arithmetic operation on incompatible datatypes: $leftDt and $rightDt") + } + DataType.FLOAT -> when(rightDt) { + DataType.BYTE -> DataType.FLOAT + DataType.WORD -> DataType.FLOAT + DataType.FLOAT -> DataType.FLOAT + else -> throw FatalAstException("arithmetic operation on incompatible datatypes: $leftDt and $rightDt") + } + else -> throw FatalAstException("arithmetic operation on incompatible datatypes: $leftDt and $rightDt") + } + } } private data class ByteOrWordLiteral(val intvalue: Int, val datatype: DataType) { @@ -742,6 +783,17 @@ data class LiteralValue(val bytevalue: Short? = null, override fun toString(): String { return "LiteralValue(byte=$bytevalue, word=$wordvalue, float=$floatvalue, str=$strvalue, array=$arrayvalue pos=$position)" } + + override fun resultingDatatype(namespace: INameScope): DataType? { + return when { + isByte -> DataType.BYTE + isWord -> DataType.WORD + isFloat -> DataType.FLOAT + isString -> DataType.STR // @todo which string? + isArray -> DataType.ARRAY // @todo byte/word array? + else -> throw FatalAstException("literalvalue has no value") + } + } } @@ -758,6 +810,19 @@ class RangeExpr(var from: IExpression, var to: IExpression) : IExpression { override fun constValue(namespace: INameScope): LiteralValue? = null override fun process(processor: IAstProcessor) = processor.process(this) override fun referencesIdentifier(name: String): Boolean = from.referencesIdentifier(name) || to.referencesIdentifier(name) + override fun resultingDatatype(namespace: INameScope): DataType? { + val fromDt=from.resultingDatatype(namespace) + val toDt=to.resultingDatatype(namespace) + return when { + fromDt==null || toDt==null -> null + fromDt==DataType.WORD || toDt==DataType.WORD -> DataType.WORD + fromDt==DataType.STR || toDt==DataType.STR -> DataType.STR + fromDt==DataType.STR_P || toDt==DataType.STR_P -> DataType.STR_P + fromDt==DataType.STR_S || toDt==DataType.STR_S -> DataType.STR_S + fromDt==DataType.STR_PS || toDt==DataType.STR_PS -> DataType.STR_PS + else -> DataType.BYTE + } + } } @@ -776,6 +841,13 @@ class RegisterExpr(val register: Register) : IExpression { override fun toString(): String { return "RegisterExpr(register=$register, pos=$position)" } + + override fun resultingDatatype(namespace: INameScope): DataType? { + return when(register){ + Register.A, Register.X, Register.Y -> DataType.BYTE + Register.AX, Register.AY, Register.XY -> DataType.WORD + } + } } @@ -811,6 +883,15 @@ data class IdentifierReference(val nameInSource: List) : IExpression { override fun process(processor: IAstProcessor) = processor.process(this) override fun referencesIdentifier(name: String): Boolean = nameInSource.last() == name // @todo is this correct all the time? + + override fun resultingDatatype(namespace: INameScope): DataType? { + val targetStmt = targetStatement(namespace) + if(targetStmt is VarDecl) { + return targetStmt.datatype + } else { + throw FatalAstException("cannot get datatype from identifier reference ${this}, pos=$position") + } + } } @@ -858,11 +939,13 @@ class FunctionCall(override var target: IdentifierReference, override var arglis arglist.forEach { it.linkParents(this) } } - override fun constValue(namespace: INameScope): LiteralValue? { + override fun constValue(namespace: INameScope) = constValue(namespace, true) + + private fun constValue(namespace: INameScope, withDatatypeCheck: Boolean): LiteralValue? { // if the function is a built-in function and the args are consts, should try to const-evaluate! if(target.nameInSource.size>1) return null try { - return when (target.nameInSource[0]) { + val resultValue = when (target.nameInSource[0]) { "sin" -> builtinSin(arglist, position, namespace) "cos" -> builtinCos(arglist, position, namespace) "abs" -> builtinAbs(arglist, position, namespace) @@ -897,6 +980,23 @@ class FunctionCall(override var target: IdentifierReference, override var arglis "P_irqd" -> throw ExpressionError("builtin function P_irqd can't be used in expressions because it doesn't return a value", position) else -> null } + if(withDatatypeCheck) { + val resultDt = this.resultingDatatype(namespace) + if(resultValue==null) + return resultValue + when(resultDt) { + DataType.BYTE -> if(resultValue.isByte) return resultValue + DataType.WORD -> if(resultValue.isWord) return resultValue + DataType.FLOAT -> if(resultValue.isFloat) return resultValue + DataType.STR, DataType.STR_P, DataType.STR_S, DataType.STR_PS -> if(resultValue.isString) return resultValue + DataType.ARRAY -> if(resultValue.isArray) return resultValue + DataType.ARRAY_W -> if(resultValue.isArray) return resultValue + DataType.MATRIX -> TODO("expected matrix as constvalue this is not yet supported") + } + throw FatalAstException("evaluated const expression result value doesn't match expected datatype $resultDt, pos=$position") + } else { + return resultValue + } } catch(x: NotConstArgumentException) { // const-evaluating the builtin function call failed. @@ -910,6 +1010,34 @@ class FunctionCall(override var target: IdentifierReference, override var arglis override fun process(processor: IAstProcessor) = processor.process(this) override fun referencesIdentifier(name: String): Boolean = target.referencesIdentifier(name) || arglist.any{it.referencesIdentifier(name)} + + override fun resultingDatatype(namespace: INameScope): DataType? { + val constVal = constValue(namespace, false) + if(constVal!=null) + return constVal.resultingDatatype(namespace) + val stmt = target.targetStatement(namespace) + if(stmt is BuiltinFunctionStatementPlaceholder) { + if(target.nameInSource[0] == "P_carry" || target.nameInSource[0]=="P_irqd") { + return null // these have no return value + } + return DataType.BYTE // @todo table lookup to determine result type of builtin function call + } + else if(stmt is Subroutine) { + if(stmt.returnvalues.isEmpty()) { + return null // no return value + } + if(stmt.returnvalues.size==1) { + return when(stmt.returnvalues[0].register) { + Register.A, Register.X, Register.Y -> DataType.BYTE + Register.AX, Register.AY, Register.XY -> DataType.WORD + else -> TODO("return type for non-register result from subroutine $stmt") + } + } + TODO("return type for subroutine with multiple return values $stmt") + } + TODO("datatype of functioncall to $stmt") + } + } diff --git a/il65/src/il65/ast/AstChecker.kt b/il65/src/il65/ast/AstChecker.kt index 2ad45e3f9..08f629a4d 100644 --- a/il65/src/il65/ast/AstChecker.kt +++ b/il65/src/il65/ast/AstChecker.kt @@ -165,24 +165,16 @@ class AstChecker(private val namespace: INameScope, private val compilerOptions: if(constVal!=null) { checkValueTypeAndRange(targetDatatype, null, assignment.value as LiteralValue, assignment.position) } else { - val sourceDatatype: DataType = when(assignment.value) { - is RegisterExpr -> { - when((assignment.value as RegisterExpr).register) { - Register.A, Register.X, Register.Y -> DataType.BYTE - Register.AX, Register.AY, Register.XY -> DataType.WORD - } - } - is IdentifierReference -> { - val targetStmt = (assignment.value as IdentifierReference).targetStatement(namespace) - if(targetStmt is VarDecl) { - targetStmt.datatype - } else { - throw FatalAstException("cannot get datatype from assignment value ${assignment.value}, pos=${assignment.position}") - } - } - else -> TODO("check assignment compatibility for value ${assignment.value}, pos=${assignment.position}") + val sourceDatatype: DataType? = assignment.value.resultingDatatype(namespace) + if(sourceDatatype==null) { + if(assignment.value is FunctionCall) + checkResult.add(ExpressionError("function call doesn't return a value to use in assignment", assignment.value.position)) + else + checkResult.add(ExpressionError("assignment source ${assignment.value} is no value or has no proper datatype", assignment.value.position)) + } + else { + checkAssignmentCompatible(targetDatatype, sourceDatatype, assignment.position) } - checkAssignmentCompatible(targetDatatype, sourceDatatype, assignment.position) } return super.process(assignment) @@ -539,7 +531,13 @@ class AstChecker(private val namespace: INameScope, private val compilerOptions: if(result) return true - checkResult.add(ExpressionError("cannot assign ${sourceDatatype.toString().toLowerCase()} to ${targetDatatype.toString().toLowerCase()}", position)) + if(sourceDatatype==DataType.WORD && targetDatatype==DataType.BYTE) + checkResult.add(ExpressionError("cannot assign word to byte, use msb() or lsb()?", position)) + else if(sourceDatatype==DataType.FLOAT && (targetDatatype==DataType.BYTE || targetDatatype==DataType.WORD)) + checkResult.add(ExpressionError("cannot assign ${sourceDatatype.toString().toLowerCase()} to ${targetDatatype.toString().toLowerCase()}; possible loss of precision", position)) + else + checkResult.add(ExpressionError("cannot assign ${sourceDatatype.toString().toLowerCase()} to ${targetDatatype.toString().toLowerCase()}", position)) + return false } } diff --git a/il65/src/il65/functions/BuiltinFunctions.kt b/il65/src/il65/functions/BuiltinFunctions.kt index f40883f83..8e05fc35d 100644 --- a/il65/src/il65/functions/BuiltinFunctions.kt +++ b/il65/src/il65/functions/BuiltinFunctions.kt @@ -172,15 +172,13 @@ fun builtinAvg(args: List, position: Position?, namespace:INameScop fun builtinLen(args: List, position: Position?, namespace:INameScope): LiteralValue { if(args.size!=1) - throw SyntaxError("len requires one non-scalar argument", position) - val iterable = args[0].constValue(namespace) - if(iterable?.arrayvalue == null) - throw SyntaxError("len requires one non-scalar argument", position) - val constants = iterable.arrayvalue.map { it.constValue(namespace)?.asNumericValue } - if(constants.contains(null)) - throw NotConstArgumentException() - val result = (constants.map { it!!.toDouble() }).size - return numericLiteral(result, args[0].position) + throw SyntaxError("len requires one argument", position) + val argument = args[0].constValue(namespace) ?: throw NotConstArgumentException() + return when { + argument.isArray -> numericLiteral(argument.arrayvalue!!.size, args[0].position) + argument.isString -> numericLiteral(argument.strvalue!!.length, args[0].position) + else -> throw FatalAstException("len of weird argument ${args[0]}") + } } fun builtinAny(args: List, position: Position?, namespace:INameScope): LiteralValue