From a0594cbce3cbc5daade643ad9b4e2da13921a916 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Thu, 28 Dec 2023 19:58:45 +0100 Subject: [PATCH] const optimizer now knows about a bunch of library functions, such as math.* --- codeOptimizers/build.gradle | 1 + codeOptimizers/codeOptimizers.iml | 1 + .../src/prog8/optimizer/ConstExprEvaluator.kt | 160 +++++++++++++++++- .../optimizer/ConstantFoldingOptimizer.kt | 9 +- compiler/res/prog8lib/string.p8 | 2 +- compiler/test/TestOptimization.kt | 23 +++ docs/source/index.rst | 2 +- docs/source/programming.rst | 6 +- docs/source/syntaxreference.rst | 3 +- docs/source/todo.rst | 2 +- examples/test.p8 | 19 +-- 11 files changed, 204 insertions(+), 24 deletions(-) diff --git a/codeOptimizers/build.gradle b/codeOptimizers/build.gradle index 75889f6fd..b5f3b6b5e 100644 --- a/codeOptimizers/build.gradle +++ b/codeOptimizers/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation project(':codeCore') implementation project(':compilerAst') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "com.michael-bull.kotlin-result:kotlin-result-jvm:1.1.18" // implementation "org.jetbrains.kotlin:kotlin-reflect" } diff --git a/codeOptimizers/codeOptimizers.iml b/codeOptimizers/codeOptimizers.iml index 9ba8b676e..4e52dfb59 100644 --- a/codeOptimizers/codeOptimizers.iml +++ b/codeOptimizers/codeOptimizers.iml @@ -11,5 +11,6 @@ + \ No newline at end of file diff --git a/codeOptimizers/src/prog8/optimizer/ConstExprEvaluator.kt b/codeOptimizers/src/prog8/optimizer/ConstExprEvaluator.kt index c1efa7bd4..06f8fe140 100644 --- a/codeOptimizers/src/prog8/optimizer/ConstExprEvaluator.kt +++ b/codeOptimizers/src/prog8/optimizer/ConstExprEvaluator.kt @@ -1,17 +1,19 @@ package prog8.optimizer +import prog8.ast.Program import prog8.ast.base.ExpressionError import prog8.ast.base.FatalAstException -import prog8.ast.expressions.Expression +import prog8.ast.expressions.FunctionCallExpression import prog8.ast.expressions.NumericLiteral import prog8.code.core.DataType import prog8.code.core.IntegerDatatypes import prog8.code.core.Position +import kotlin.math.* class ConstExprEvaluator { - fun evaluate(left: NumericLiteral, operator: String, right: NumericLiteral): Expression { + fun evaluate(left: NumericLiteral, operator: String, right: NumericLiteral): NumericLiteral { try { return when(operator) { "+" -> plus(left, right) @@ -40,7 +42,7 @@ class ConstExprEvaluator { } } - private fun shiftedright(left: NumericLiteral, amount: NumericLiteral): Expression { + private fun shiftedright(left: NumericLiteral, amount: NumericLiteral): NumericLiteral { if(left.type !in IntegerDatatypes || amount.type !in IntegerDatatypes) throw ExpressionError("cannot compute $left >> $amount", left.position) val result = @@ -51,7 +53,7 @@ class ConstExprEvaluator { return NumericLiteral(left.type, result.toDouble(), left.position) } - private fun shiftedleft(left: NumericLiteral, amount: NumericLiteral): Expression { + private fun shiftedleft(left: NumericLiteral, amount: NumericLiteral): NumericLiteral { if(left.type !in IntegerDatatypes || amount.type !in IntegerDatatypes) throw ExpressionError("cannot compute $left << $amount", left.position) val result = left.number.toInt().shl(amount.number.toInt()) @@ -218,4 +220,154 @@ class ConstExprEvaluator { else -> throw ExpressionError(error, left.position) } } + + fun evaluate(call: FunctionCallExpression, program: Program): NumericLiteral? { + if(call.target.nameInSource.size!=2) + return null // likely a builtin function, or user function, these get evaluated elsewhere + val constArgs = call.args.mapNotNull { it.constValue(program) } + if(constArgs.size!=call.args.size) + return null + + return when(call.target.nameInSource[0]) { + "math" -> evalMath(call, constArgs) + "floats" -> evalFloats(call, constArgs) + "string" -> evalString(call, constArgs) + else -> null + } + } + + private fun evalFloats(func: FunctionCallExpression, args: List): NumericLiteral? { + val result= when(func.target.nameInSource[1]) { + "pow" -> args[0].number.pow(args[1].number) + "sin" -> sin(args[0].number) + "cos" -> cos(args[0].number) + "tan" -> tan(args[0].number) + "atan" -> atan(args[0].number) + "ln" -> ln(args[0].number) + "log2" -> log2(args[0].number) + "rad" -> args[0].number/360.0 * 2 * PI + "deg" -> args[0].number/ 2 / PI * 360.0 + "round" -> round(args[0].number) + "floor" -> floor(args[0].number) + "ceil" -> ceil(args[0].number) + "minf", "min" -> min(args[0].number, args[1].number) + "maxf", "max" -> max(args[0].number, args[1].number) + "clampf", "clamp" -> { + var value = args[0].number + val minimum = args[1].number + val maximum = args[2].number + if(value null + } + + return if(result==null) + null + else + NumericLiteral(DataType.FLOAT, result, func.position) + } + + private fun evalMath(func: FunctionCallExpression, args: List): NumericLiteral? { + return when(func.target.nameInSource[1]) { + "sin8u" -> { + val value = truncate(128.0 + 127.5 * sin(args.single().number / 256.0 * 2 * PI)) + NumericLiteral(DataType.UBYTE, value, func.position) + } + "cos8u" -> { + val value = truncate(128.0 + 127.5 * cos(args.single().number / 256.0 * 2 * PI)) + NumericLiteral(DataType.UBYTE, value, func.position) + } + "sin8" -> { + val value = truncate(127.0 * sin(args.single().number / 256.0 * 2 * PI)) + NumericLiteral(DataType.BYTE, value, func.position) + } + "cos8" -> { + val value = truncate(127.0 * cos(args.single().number / 256.0 * 2 * PI)) + NumericLiteral(DataType.BYTE, value, func.position) + } + "sinr8u" -> { + val value = truncate(128.0 + 127.5 * sin(args.single().number / 180.0 * 2 * PI)) + NumericLiteral(DataType.UBYTE, value, func.position) + } + "cosr8u" -> { + val value = truncate(128.0 + 127.5 * cos(args.single().number / 180.0 * 2 * PI)) + NumericLiteral(DataType.UBYTE, value, func.position) + } + "sinr8" -> { + val value = truncate(127.0 * sin(args.single().number / 180.0 * 2 * PI)) + NumericLiteral(DataType.BYTE, value, func.position) + } + "cosr8" -> { + val value = truncate(127.0 * cos(args.single().number / 180.0 * 2 * PI)) + NumericLiteral(DataType.BYTE, value, func.position) + } + "log2" -> { + val value = truncate(log2(args.single().number)) + NumericLiteral(DataType.UBYTE, value, func.position) + } + "log2w" -> { + val value = truncate(log2(args.single().number)) + NumericLiteral(DataType.UWORD, value, func.position) + } + "atan2" -> { + val x1f = args[0].number + val y1f = args[1].number + val x2f = args[2].number + val y2f = args[3].number + var radians = atan2(y2f-y1f, x2f-x1f) + if(radians<0) + radians+=2*PI + NumericLiteral(DataType.UWORD, floor(radians/2.0/PI*256.0), func.position) + } + "diff" -> { + val n1 = args[0].number + val n2 = args[1].number + val value = if(n1>n2) n1-n2 else n2-n1 + NumericLiteral(DataType.UBYTE, value, func.position) + } + "diffw" -> { + val n1 = args[0].number + val n2 = args[1].number + val value = if(n1>n2) n1-n2 else n2-n1 + NumericLiteral(DataType.UWORD, value, func.position) + } + else -> null + } + } + + private fun evalString(func: FunctionCallExpression, args: List): NumericLiteral? { + return when(func.target.nameInSource[1]) { + "isdigit" -> { + val char = args[0].number.toInt() + NumericLiteral.fromBoolean(char in 48..57, func.position) + } + "isupper" -> { + // shifted petscii has 2 ranges that contain the upper case letters... 97-122 and 193-218 + val char = args[0].number.toInt() + NumericLiteral.fromBoolean(char in 97..122 || char in 193..218, func.position) + } + "islower" -> { + val char = args[0].number.toInt() + NumericLiteral.fromBoolean(char in 65..90, func.position) + } + "isletter" -> { + val char = args[0].number.toInt() + NumericLiteral.fromBoolean(char in 65..90 || char in 97..122 || char in 193..218, func.position) + } + "isspace" -> { + val char = args[0].number.toInt() + NumericLiteral.fromBoolean(char in arrayOf(32, 13, 9, 10, 141, 160), func.position) + } + "isprint" -> { + val char = args[0].number.toInt() + NumericLiteral.fromBoolean(char in 32..127 || char>=160, func.position) + } + else -> null + } + } } diff --git a/codeOptimizers/src/prog8/optimizer/ConstantFoldingOptimizer.kt b/codeOptimizers/src/prog8/optimizer/ConstantFoldingOptimizer.kt index e79fcb596..07cba545b 100644 --- a/codeOptimizers/src/prog8/optimizer/ConstantFoldingOptimizer.kt +++ b/codeOptimizers/src/prog8/optimizer/ConstantFoldingOptimizer.kt @@ -319,8 +319,13 @@ class ConstantFoldingOptimizer(private val program: Program, private val errors: val constvalue = functionCallExpr.constValue(program) return if(constvalue!=null) listOf(IAstModification.ReplaceNode(functionCallExpr, constvalue, parent)) - else - noModifications + else { + val const2 = evaluator.evaluate(functionCallExpr, program) + return if(const2!=null) + listOf(IAstModification.ReplaceNode(functionCallExpr, const2, parent)) + else + noModifications + } } override fun after(bfc: BuiltinFunctionCall, parent: Node): Iterable { diff --git a/compiler/res/prog8lib/string.p8 b/compiler/res/prog8lib/string.p8 index 915bfda4b..2cf7d3148 100644 --- a/compiler/res/prog8lib/string.p8 +++ b/compiler/res/prog8lib/string.p8 @@ -399,7 +399,7 @@ fail clc ; yes, no match found, return with c=0 } asmsub isupper(ubyte petsciichar @A) -> bool @Pc { - ; shifted petscii has 2 ranges that contain the upper case letters... + ; shifted petscii has 2 ranges that contain the upper case letters... 97-122 and 193-218 %asm {{ cmp #97 bcs + diff --git a/compiler/test/TestOptimization.kt b/compiler/test/TestOptimization.kt index 9898ad0ae..208ef0b7e 100644 --- a/compiler/test/TestOptimization.kt +++ b/compiler/test/TestOptimization.kt @@ -899,4 +899,27 @@ main { (expr.left as? IdentifierReference)?.nameInSource shouldBe listOf("yy") (expr.right as? NumericLiteral)?.number shouldBe 10.0 } + + test("advanced const folding of known library functions") { + val src=""" +%import floats +%import math +%import string + +main { + sub start() { + float fl = 1.2 ; no other assignments + cx16.r0L = string.isdigit(math.diff(119, floats.floor(floats.deg(fl)) as ubyte)) + cx16.r1L = string.isletter(math.diff(119, floats.floor(floats.deg(1.2)) as ubyte)) + } +}""" + val result = compileText(Cx16Target(), true, src, writeAssembly = false)!! + val st = result.compilerAst.entrypoint.statements + st.size shouldBe 3 + (st[0] as VarDecl).type shouldBe VarDeclType.CONST + val assignv1 = (st[1] as Assignment).value + val assignv2 = (st[2] as Assignment).value + (assignv1 as NumericLiteral).number shouldBe 1.0 + (assignv2 as NumericLiteral).number shouldBe 0.0 + } }) diff --git a/docs/source/index.rst b/docs/source/index.rst index ed4ad104e..a6e070587 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -72,7 +72,7 @@ Language features - Floating point math is supported on select compiler targets. - Strings can contain escaped characters but also many symbols directly if they have a PETSCII equivalent, such as "♠♥♣♦π▚●○╳". Characters like ^, _, \\, {, } and | are also accepted and converted to the closest PETSCII equivalents. - Identifiers can contain Unicode Letters, so ``knäckebröd``, ``приблизительно``, ``見せしめ`` and ``π`` are all valid identifiers. -- High-level code optimizations, such as const-folding (zero-allocation constants that are optimized away in expressions), expression and statement simplifications/rewriting. +- Advanced code optimizations, such as const-folding (zero-allocation constants that are optimized away in expressions), expression and statement simplifications/rewriting. - Programs can be run multiple times without reloading because of automatic variable (re)initializations. - Supports the sixteen 'virtual' 16-bit registers R0 .. R15 as defined on the Commander X16, also on the other machines. - Support for low level system features such as Vera Fx hardware word multiplication on the Commander X16 diff --git a/docs/source/programming.rst b/docs/source/programming.rst index f13a78c58..d5b283c90 100644 --- a/docs/source/programming.rst +++ b/docs/source/programming.rst @@ -441,7 +441,11 @@ Special types: const and memory-mapped When using ``const``, the value of the 'variable' cannot be changed; it has become a compile-time constant value instead. You'll have to specify the initial value expression. This value is then used by the compiler everywhere you refer to the constant (and no memory is allocated -for the constant itself). This is only valid for the simple numeric types (byte, word, float). +for the constant itself). Onlythe simple numeric types (byte, word, float) can be defined as a constant. +If something is defined as a constant, very efficient code can usually be generated from it. +Variables on the other hand can't be optimized as much, need memory, and more code to manipulate them. +Note that a subset of the library routines in the ``math``, ``string`` and ``floats`` modules are recognised in +compile time expressions. For example, the compiler knows what ``math.sin8u(12)`` is and replaces it with the computed result. When using ``&`` (the address-of operator but now applied to a datatype), the variable will point to specific location in memory, rather than being newly allocated. The initial value (mandatory) must be a valid diff --git a/docs/source/syntaxreference.rst b/docs/source/syntaxreference.rst index c5af07c1d..6cefe359f 100644 --- a/docs/source/syntaxreference.rst +++ b/docs/source/syntaxreference.rst @@ -467,7 +467,8 @@ Constants All variables can be assigned new values unless you use the ``const`` keyword. The initial value must be known at compile time (it must be a compile time constant expression). -This is only valid for the simple numeric types (byte, word, float):: + +Only the simple numeric types (byte, word, float) can be defined as a constant:: const byte max_age = 99 diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 9ac78da93..f8ddf740f 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -2,7 +2,6 @@ TODO ==== -- add functions to const eval that we know how to evaluate (math.*, etc) - [on branch: shortcircuit] investigate McCarthy evaluation again? this may also reduce code size perhaps for things like if a>4 or a<2 .... ... @@ -49,6 +48,7 @@ Compiler: Libraries: +- get rid of the "f" suffix of several funtions in floats_functions (breaking change) - once a VAL_1 implementation is merged into the X16 kernal properly, remove all the workarounds in cx16 floats.parse_f() . Prototype parse routine in examples/cx16/floatparse.p8 - fix the problems in atari target, and flesh out its libraries. - c128 target: make syslib more complete (missing kernal routines)? diff --git a/examples/test.p8 b/examples/test.p8 index f73e53179..3be700990 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -1,19 +1,12 @@ -%import textio +%import floats +%import math +%import string %zeropage basicsafe main { sub start() { - ubyte x = 10 - ubyte y = 2 - txt.print_ub(5