automatically convert multi-compare expression (if X==1 or X==2..) to contaiment check if X in [1,2,..]

This commit is contained in:
Irmen de Jong 2022-05-08 13:21:34 +02:00
parent 8c4765b386
commit fef52c0112
4 changed files with 160 additions and 35 deletions

View File

@ -91,13 +91,47 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
} }
override fun before(expr: BinaryExpression, parent: Node): Iterable<IAstModification> { override fun before(expr: BinaryExpression, parent: Node): Iterable<IAstModification> {
// try to replace a multi-comparison expression (if x==1 or x==2 or x==3 ... ) by a simple containment check.
// but only if the containment check is the top-level expression.
if(parent is BinaryExpression)
return noModifications
if(expr.operator == "or") { if(expr.operator == "or") {
val leftBinExpr = expr.left as? BinaryExpression val leftBinExpr1 = expr.left as? BinaryExpression
val rightBinExpr = expr.right as? BinaryExpression val rightBinExpr1 = expr.right as? BinaryExpression
if(leftBinExpr!=null && leftBinExpr.operator=="==" && rightBinExpr!=null && rightBinExpr.operator=="==") {
if(leftBinExpr.right is NumericLiteral && rightBinExpr.right is NumericLiteral) { if(rightBinExpr1?.operator=="==" && rightBinExpr1.right is NumericLiteral && leftBinExpr1!=null) {
if(leftBinExpr.left isSameAs rightBinExpr.left) val needle = rightBinExpr1.left
errors.warn("consider using 'in' or 'when' to test for multiple values", expr.position) val values = mutableListOf(rightBinExpr1.right as NumericLiteral)
fun isMultiComparisonRecurse(expr: BinaryExpression): Boolean {
if(expr.operator=="==") {
if(expr.right is NumericLiteral && expr.left isSameAs needle) {
values.add(expr.right as NumericLiteral)
return true
}
return false
}
if(expr.operator!="or")
return false
val leftBinExpr = expr.left as? BinaryExpression
val rightBinExpr = expr.right as? BinaryExpression
if(leftBinExpr==null || rightBinExpr==null || rightBinExpr.right !is NumericLiteral || !rightBinExpr.left.isSameAs(needle))
return false
if(rightBinExpr.operator=="==")
values.add(rightBinExpr.right as NumericLiteral)
else
return false
return isMultiComparisonRecurse(leftBinExpr)
}
if(isMultiComparisonRecurse(leftBinExpr1)) {
// replace it!
val valueCopies = values.sortedBy { it.number }.map { it.copy() }
val elementType = needle.inferType(program).getOrElse { throw FatalAstException("invalid needle dt") }
val arrayType = ElementToArrayTypes.getValue(elementType)
val valuesArray = ArrayLiteral(InferredTypes.InferredType.known(arrayType), valueCopies.toTypedArray(), expr.position)
val containment = ContainmentCheck(needle, valuesArray, expr.position)
return listOf(IAstModification.ReplaceNode(expr, containment, parent))
} }
} }
} }

View File

@ -3,10 +3,12 @@ package prog8tests
import io.kotest.assertions.fail import io.kotest.assertions.fail
import io.kotest.assertions.withClue import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldStartWith
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotBeBlank import io.kotest.matchers.string.shouldNotBeBlank
import io.kotest.matchers.string.shouldStartWith
import io.kotest.matchers.types.instanceOf import io.kotest.matchers.types.instanceOf
import io.kotest.matchers.types.shouldBeSameInstanceAs import io.kotest.matchers.types.shouldBeSameInstanceAs
import prog8.ast.ParentSentinel import prog8.ast.ParentSentinel
@ -169,7 +171,6 @@ class TestOptimization: FunSpec({
} }
}""" }"""
val result = compileText(C64Target(), true, source, writeAssembly = false)!! val result = compileText(C64Target(), true, source, writeAssembly = false)!!
printProgram(result.program)
// expected: // expected:
// word llw // word llw
// llw = 300 // llw = 300
@ -424,7 +425,6 @@ class TestOptimization: FunSpec({
} }
}""" }"""
val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!!
printProgram(result.program)
result.program.entrypoint.statements.size shouldBe 3 result.program.entrypoint.statements.size shouldBe 3
val ifstmt = result.program.entrypoint.statements[0] as IfElse val ifstmt = result.program.entrypoint.statements[0] as IfElse
ifstmt.truepart.statements.size shouldBe 1 ifstmt.truepart.statements.size shouldBe 1
@ -609,7 +609,6 @@ class TestOptimization: FunSpec({
} }
""" """
val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!!
printProgram(result.program)
/* expected result: /* expected result:
uword yy uword yy
yy = 20 yy = 20
@ -649,8 +648,6 @@ class TestOptimization: FunSpec({
yy = 0 yy = 0
xx += 10 xx += 10
*/ */
printProgram(result.program)
val stmts = result.program.entrypoint.statements val stmts = result.program.entrypoint.statements
stmts.size shouldBe 7 stmts.size shouldBe 7
stmts.filterIsInstance<VarDecl>().size shouldBe 2 stmts.filterIsInstance<VarDecl>().size shouldBe 2
@ -665,4 +662,101 @@ class TestOptimization: FunSpec({
(xxValue.left as? IdentifierReference)?.nameInSource shouldBe listOf("xx") (xxValue.left as? IdentifierReference)?.nameInSource shouldBe listOf("xx")
xxValue.right shouldBe NumericLiteral(DataType.UBYTE, 10.0, Position.DUMMY) xxValue.right shouldBe NumericLiteral(DataType.UBYTE, 10.0, Position.DUMMY)
} }
test("multi-comparison replaced by containment check") {
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
if source==3 or source==4 or source==99 or source==1
thingy++
}
}"""
val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!!
/*
expected result:
ubyte[] auto_heap_var = [1,4,99,3]
ubyte source
source = 99
ubyte thingy
thingy = 42
if source in auto_heap_var
thingy++
*/
val stmts = result.program.entrypoint.statements
stmts.size shouldBe 6
val ifStmt = stmts[5] as IfElse
val containment = ifStmt.condition as ContainmentCheck
(containment.element as IdentifierReference).nameInSource shouldBe listOf("source")
(containment.iterable as IdentifierReference).nameInSource.single() shouldStartWith "auto_heap_value"
val arrayDecl = stmts[0] as VarDecl
arrayDecl.isArray shouldBe true
arrayDecl.arraysize?.constIndex() shouldBe 4
val arrayValue = arrayDecl.value as ArrayLiteral
arrayValue.type shouldBe InferredTypes.InferredType.known(DataType.ARRAY_UB)
arrayValue.value shouldBe listOf(
NumericLiteral.optimalInteger(1, Position.DUMMY),
NumericLiteral.optimalInteger(3, Position.DUMMY),
NumericLiteral.optimalInteger(4, Position.DUMMY),
NumericLiteral.optimalInteger(99, Position.DUMMY))
}
test("invalid multi-comparison (not all equals) not replaced") {
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
if source==3 or source==4 or source!=99 or source==1
thingy++
}
}"""
val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!!
printProgram(result.program)
val stmts = result.program.entrypoint.statements
stmts.size shouldBe 5
val ifStmt = stmts[4] as IfElse
ifStmt.condition shouldBe instanceOf<BinaryExpression>()
}
test("invalid multi-comparison (not all same needle) not replaced") {
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
if source==3 or source==4 or thingy==99 or source==1
thingy++
}
}"""
val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!!
printProgram(result.program)
val stmts = result.program.entrypoint.statements
stmts.size shouldBe 5
val ifStmt = stmts[4] as IfElse
ifStmt.condition shouldBe instanceOf<BinaryExpression>()
}
test("invalid multi-comparison (not all or) not replaced") {
val src="""
main {
sub start() {
ubyte source=99
ubyte thingy=42
if source==3 or source==4 and source==99 or source==1
thingy++
}
}"""
val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!!
printProgram(result.program)
val stmts = result.program.entrypoint.statements
stmts.size shouldBe 5
val ifStmt = stmts[4] as IfElse
ifStmt.condition shouldBe instanceOf<BinaryExpression>()
}
}) })

View File

@ -3,12 +3,15 @@ TODO
For next release For next release
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
- pipe operator: allow non-unary function calls in the pipe that specify the other argument(s) in the calls. - fix compiler crash when comparing strings
- make it possible to inline non-asmsub routines that just contain a single statement (return, functioncall, assignment) - make it possible to inline non-asmsub routines that just contain a single statement (return, functioncall, assignment)
but this requires all identifiers in the inlined expression to be changed to fully scoped names. Only if the arguments are simple expressions, and the inlined subroutine cannot contain further nested subroutines!
If we can do that why not perhaps also able to inline multi-line subroutines? Why would it be limited to just 1 line? Maybe to protect against code size bloat. This requires all identifiers in the inlined expression to be changed to fully scoped names (because their scope changes).
Inlined subroutines cannot contain further nested subroutines! If we can do that why not perhaps also able to inline multi-line subroutines?
Why would it be limited to just 1 line? Maybe to protect against code size bloat.
Once this works, look for library subroutines that should be inlined. Once this works, look for library subroutines that should be inlined.
- vm: add way more instructions operating directly on memory instead of only registers
- add McCarthy evaluation to shortcircuit and/or expressions. First do ifs by splitting them up? Then do expressions that compute a value?
... ...
@ -24,7 +27,6 @@ Future Things and Ideas
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
Compiler: Compiler:
- vm: add way more instructions operating directly on memory instead of only registers
- vm: codeGen: various TODOs to tweak code - vm: codeGen: various TODOs to tweak code
- vm: somehow deal with asmsubs otherwise the vm IR can't fully encode all of prog8 - vm: somehow deal with asmsubs otherwise the vm IR can't fully encode all of prog8
- vm: make registers typed? so that it's immediately obvious what type they represent. Much like regular variables in memory. - vm: make registers typed? so that it's immediately obvious what type they represent. Much like regular variables in memory.
@ -40,7 +42,6 @@ Compiler:
- make everything an expression? (get rid of Statements. Statements are expressions with void return types?). - make everything an expression? (get rid of Statements. Statements are expressions with void return types?).
- simplifyConditionalExpression() should not split expression if it still results in stack-based evaluation, but how does it know? - simplifyConditionalExpression() should not split expression if it still results in stack-based evaluation, but how does it know?
- simplifyConditionalExpression() sometimes introduces needless assignment to r9 tempvar (what scenarios?) - simplifyConditionalExpression() sometimes introduces needless assignment to r9 tempvar (what scenarios?)
- add McCarthy evaluation to shortcircuit and/or expressions. First do ifs by splitting them up? Then do expressions that compute a value?
- make it possible to use cpu opcodes such as 'nop' as variable names by prefixing all asm vars with something such as ``p8v_``? Or not worth it (most 3 letter opcodes as variables are nonsensical anyway) - make it possible to use cpu opcodes such as 'nop' as variable names by prefixing all asm vars with something such as ``p8v_``? Or not worth it (most 3 letter opcodes as variables are nonsensical anyway)
then we can get rid of the instruction lists in the machinedefinitions as well? then we can get rid of the instruction lists in the machinedefinitions as well?
- [problematic due to using 64tass:] add a compiler option to not remove unused subroutines. this allows for building library programs. But this won't work with 64tass's .proc ... - [problematic due to using 64tass:] add a compiler option to not remove unused subroutines. this allows for building library programs. But this won't work with 64tass's .proc ...
@ -58,12 +59,12 @@ Libraries:
- optimize several inner loops in gfx2 even further? - optimize several inner loops in gfx2 even further?
- add modes 2 and 3 to gfx2 (lowres 4 color and 16 color)? - add modes 2 and 3 to gfx2 (lowres 4 color and 16 color)?
- add a flood fill routine to gfx2? - add a flood fill routine to gfx2?
- diskio: use cx16 MACPTR() to load stuff faster? (see it's use in X16edit to fast load blocks) - diskio: use cx16 MACPTR() to load stuff faster? (see its use in X16edit to fast load blocks)
note that it might fail on non sdcard files so have to make graceful degradation note that it might fail on non sdcard files so have to make graceful degradation
- add a diskio.f_seek() routine for the Cx16 that uses its seek dos api? (only if that's stable)
Expressions: Expressions:
- pipe operator: (targets other than 'Virtual'): allow non-unary function calls in the pipe that specify the other argument(s) in the calls.
- rethink the whole "isAugmentable" business. Because the way this is determined, should always also be exactly mirrorred in the AugmentableAssignmentAsmGen or you'll get a crash at code gen time. - rethink the whole "isAugmentable" business. Because the way this is determined, should always also be exactly mirrorred in the AugmentableAssignmentAsmGen or you'll get a crash at code gen time.
note: new ast PtAssignment already has no knowledge about this anymore. note: new ast PtAssignment already has no knowledge about this anymore.
- can we get rid of pieces of asmgen.AssignmentAsmGen by just reusing the AugmentableAssignment ? generated code should not suffer - can we get rid of pieces of asmgen.AssignmentAsmGen by just reusing the AugmentableAssignment ? generated code should not suffer
@ -84,5 +85,4 @@ Optimizations:
- AssignmentAsmGen.assignExpression() -> better code gen for assigning boolean comparison expressions - AssignmentAsmGen.assignExpression() -> better code gen for assigning boolean comparison expressions
- when a for loop's loopvariable isn't referenced in the body, and the iterations are known, replace the loop by a repeatloop - when a for loop's loopvariable isn't referenced in the body, and the iterations are known, replace the loop by a repeatloop
but we have no efficient way right now to see if the body references a variable. but we have no efficient way right now to see if the body references a variable.
- automatically convert if statements that test for multiple values (if X==1 or X==2..) to if X in [1,2,..] statements, instead of just a warning.
- introduce byte-index operator to avoid index multiplications in loops over arrays? see github issue #4 - introduce byte-index operator to avoid index multiplications in loops over arrays? see github issue #4

View File

@ -7,23 +7,20 @@
main { main {
sub add(ubyte first, ubyte second) -> ubyte {
return first+second
}
sub mul(ubyte first, ubyte second) -> ubyte {
return first*second
}
sub start() { sub start() {
str thing = "????"
if thing=="bmap" {
txt.print("gottem")
}
; TODO: test with builtin function using multiple args (such as mkword) ; TODO: test with builtin function using multiple args (such as mkword)
ubyte source=99 ; ubyte value = add(3,4) |> add(10) |> mul(2) |> math.sin8u() ; TODO should not work yet on vm codegen, but it compiles.... :/
ubyte value = add(3,4) |> add(10) |> mul(2) |> math.sin8u() ; TODO should not work yet on vm codegen, but it compiles.... :/ ; txt.print_ub(value)
txt.print_ub(value) ; txt.nl()
txt.nl() ; uword wvalue = add(3,4) |> add($30) |> mkword($ea)
uword wvalue = add(3,4) |> add($30) |> mkword($ea) ; txt.print_uwhex(wvalue, true)
txt.print_uwhex(wvalue, true) ; txt.nl()
txt.nl()
; expected output: aaabbb aaa bbb ; expected output: aaabbb aaa bbb