From 6aed7e429a17a55ebef3435ea8067e7e6824f5cb Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Wed, 3 Jan 2024 01:17:09 +0100 Subject: [PATCH] allow containment check in a range expression ("run time" range expression) --- compiler/src/prog8/compiler/Compiler.kt | 9 +++ .../compiler/astprocessing/AstChecker.kt | 24 +++--- .../astprocessing/BeforeAsmAstChanger.kt | 8 -- .../astprocessing/IntermediateAstMaker.kt | 74 +++++++++++++++++-- compiler/test/TestCompilerOnRanges.kt | 8 ++ compiler/test/ast/TestProg8Parser.kt | 2 +- docs/source/syntaxreference.rst | 9 ++- docs/source/todo.rst | 7 -- examples/test.p8 | 53 +++++++++++-- 9 files changed, 149 insertions(+), 45 deletions(-) diff --git a/compiler/src/prog8/compiler/Compiler.kt b/compiler/src/prog8/compiler/Compiler.kt index 2c9503064..08b1e8231 100644 --- a/compiler/src/prog8/compiler/Compiler.kt +++ b/compiler/src/prog8/compiler/Compiler.kt @@ -146,6 +146,15 @@ fun compileProgram(args: CompilerArguments): CompilationResult? { return null } ast = intermediateAst + } else { + if(args.printAst1) { + println("\n*********** COMPILER AST *************") + printProgram(program) + println("*********** COMPILER AST END *************\n") + } + if(args.printAst2) { + System.err.println("There is no intermediate Ast available if assembly generation is disabled.") + } } } diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index 397b28837..b0cdf63d8 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -1447,19 +1447,21 @@ internal class AstChecker(private val program: Program, } } - if(iterableDt.isIterable && containment.iterable !is RangeExpression) { - 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 (iterableDt.isIterable) { + if (containment.iterable !is RangeExpression) { + 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) } - if (invalidDt) - errors.err("element datatype doesn't match iterable datatype", containment.position) } else { - errors.err("value set for containment check must be a string or array", containment.iterable.position) + errors.err("iterable must be an array, a string, or a range expression", containment.iterable.position) } super.visit(containment) diff --git a/compiler/src/prog8/compiler/astprocessing/BeforeAsmAstChanger.kt b/compiler/src/prog8/compiler/astprocessing/BeforeAsmAstChanger.kt index 202ac0a25..41d729ac0 100644 --- a/compiler/src/prog8/compiler/astprocessing/BeforeAsmAstChanger.kt +++ b/compiler/src/prog8/compiler/astprocessing/BeforeAsmAstChanger.kt @@ -5,8 +5,6 @@ import prog8.ast.Node import prog8.ast.Program import prog8.ast.base.FatalAstException import prog8.ast.expressions.BinaryExpression -import prog8.ast.expressions.ContainmentCheck -import prog8.ast.expressions.IdentifierReference import prog8.ast.expressions.NumericLiteral import prog8.ast.statements.* import prog8.ast.walk.AstWalker @@ -28,12 +26,6 @@ internal class BeforeAsmAstChanger(val program: Program, private val options: Co throw InternalCompilerException("do..until should have been converted to jumps") } - override fun after(containment: ContainmentCheck, parent: Node): Iterable { - if(containment.iterable !is IdentifierReference) - throw InternalCompilerException("iterable in containmentcheck should be identifier (referencing string or array)") - return noModifications - } - override fun after(decl: VarDecl, parent: Node): Iterable { if (decl.type == VarDeclType.VAR && decl.value != null && decl.datatype in NumericDatatypes) throw InternalCompilerException("vardecls for variables, with initial numerical value, should have been rewritten as plain vardecl + assignment $decl") diff --git a/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt b/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt index b25dc2dcc..2089002c9 100644 --- a/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt +++ b/compiler/src/prog8/compiler/astprocessing/IntermediateAstMaker.kt @@ -583,16 +583,74 @@ class IntermediateAstMaker(private val program: Program, private val errors: IEr return call } - private fun transform(srcCheck: ContainmentCheck): PtContainmentCheck { - val check = PtContainmentCheck(srcCheck.position) - check.add(transformExpression(srcCheck.element)) - if(srcCheck.iterable !is IdentifierReference) - throw FatalAstException("iterable in containmentcheck must always be an identifier (referencing string or array) $srcCheck") - val iterable = transformExpression(srcCheck.iterable) - check.add(iterable) - return check + private fun transform(srcCheck: ContainmentCheck): PtExpression { + + fun desugar(range: RangeExpression): PtExpression { + val expr = PtBinaryExpression("and", DataType.UBYTE, srcCheck.position) + val x1 = transformExpression(srcCheck.element) + val x2 = transformExpression(srcCheck.element) + val eltDt = srcCheck.element.inferType(program) + if(eltDt.isInteger) { + val low = PtBinaryExpression("<=", DataType.UBYTE, srcCheck.position) + low.add(transformExpression(range.from)) + low.add(x1) + expr.add(low) + val high = PtBinaryExpression("<=", DataType.UBYTE, srcCheck.position) + high.add(x2) + high.add(transformExpression(range.to)) + expr.add(high) + } else { + val low = PtBinaryExpression("<=", DataType.UBYTE, srcCheck.position) + val lowFloat = PtTypeCast(DataType.FLOAT, range.from.position) + lowFloat.add(transformExpression(range.from)) + low.add(lowFloat) + low.add(x1) + expr.add(low) + val high = PtBinaryExpression("<=", DataType.UBYTE, srcCheck.position) + high.add(x2) + val highFLoat = PtTypeCast(DataType.FLOAT, range.to.position) + highFLoat.add(transformExpression(range.to)) + high.add(highFLoat) + expr.add(high) + } + return expr + } + + when(srcCheck.iterable) { + is IdentifierReference -> { + val check = PtContainmentCheck(srcCheck.position) + check.add(transformExpression(srcCheck.element)) + val iterable = transformExpression(srcCheck.iterable) + check.add(iterable) + return check + } + is RangeExpression -> { + val range = srcCheck.iterable as RangeExpression + val constRange = range.toConstantIntegerRange() + val constElt = srcCheck.element.constValue(program)?.number + val step = range.step.constValue(program)?.number + if(constElt!=null && constRange!=null) { + return PtNumber(DataType.UBYTE, if(constRange.first<=constElt && constElt<=constRange.last) 1.0 else 0.0, srcCheck.position) + } + else if(step==1.0) { + // x in low to high --> low <=x and x <= high + return desugar(range) + } else if(step==-1.0) { + // x in high downto low -> low <=x and x <= high + val tmp = range.to + range.to = range.from + range.from = tmp + return desugar(range) + } else { + errors.err("cannot use step size different than 1 or -1 in a non constant range containment check", srcCheck.position) + return PtNumber(DataType.BYTE, 0.0, Position.DUMMY) + } + } + else -> throw FatalAstException("iterable in containmentcheck must always be an identifier (referencing string or array) or a range expression $srcCheck") + } } + private fun transform(memory: DirectMemoryWrite): PtMemoryByte { val mem = PtMemoryByte(memory.position) mem.add(transformExpression(memory.addressExpression)) diff --git a/compiler/test/TestCompilerOnRanges.kt b/compiler/test/TestCompilerOnRanges.kt index 617d57f56..c23ab01f3 100644 --- a/compiler/test/TestCompilerOnRanges.kt +++ b/compiler/test/TestCompilerOnRanges.kt @@ -404,6 +404,14 @@ class TestCompilerOnRanges: FunSpec({ if ww in wvalues { xx++ } + + if xx in 10 to 20 { + xx++ + } + + if ww in 1000 to 2000 { + xx++ + } } }""", writeAssembly = true) shouldNotBe null } diff --git a/compiler/test/ast/TestProg8Parser.kt b/compiler/test/ast/TestProg8Parser.kt index 80dd78b01..389d2a856 100644 --- a/compiler/test/ast/TestProg8Parser.kt +++ b/compiler/test/ast/TestProg8Parser.kt @@ -924,7 +924,7 @@ class TestProg8Parser: FunSpec( { val errors = ErrorReporterForTests() compileText(C64Target(), false, text, writeAssembly = false, errors = errors) shouldBe null errors.errors.size shouldBe 2 - errors.errors[0] shouldContain "must be a string or array" + errors.errors[0] shouldContain "iterable must be" errors.errors[1] shouldContain "datatype doesn't match" } diff --git a/docs/source/syntaxreference.rst b/docs/source/syntaxreference.rst index 408b23b46..af6b7f8d0 100644 --- a/docs/source/syntaxreference.rst +++ b/docs/source/syntaxreference.rst @@ -611,10 +611,11 @@ range creation: ``to``, ``downto`` See :ref:`range-expression` for details. containment check: ``in`` - Tests if a value is present in a list of values, which can be a string or an array. + Tests if a value is present in a list of values, which can be a string, or an array, or a range expression. The result is a simple boolean ``true`` or ``false``. Consider using this instead of chaining multiple value tests with ``or``, because the containment check is more efficient. + Checking N in a range from x to y, is identical to x<=N and N<=y; the actual range of values is never created. Examples:: ubyte cc @@ -622,7 +623,11 @@ containment check: ``in`` txt.print("cc is one of the values") } - str email_address = "name@test.com" + if cc in 10 to 20 { + txt.print("10 <= cc and cc <=20") + } + + str email_address = "name@test.com" if '@' in email_address { txt.print("email address seems ok") } diff --git a/docs/source/todo.rst b/docs/source/todo.rst index cf8367024..f76e29932 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -76,10 +76,3 @@ Other language/syntax features to think about - add (rom/ram)bank support to romsub. A call will then automatically switch banks, use callfar and something else when in banked ram. challenges: how to not make this too X16 specific? How does the compiler know what bank to switch (ram/rom)? How to make it performant when we want to (i.e. NOT have it use callfar/auto bank switching) ? -- chained comparisons `10