package prog8tests.compiler import io.kotest.assertions.withClue import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldStartWith import io.kotest.matchers.types.instanceOf import io.kotest.matchers.types.shouldBeSameInstanceAs import prog8.ast.ParentSentinel import prog8.ast.Program import prog8.ast.expressions.* import prog8.ast.statements.* import prog8.code.ast.PtAssignTarget import prog8.code.ast.PtAssignment import prog8.code.ast.PtFunctionCall import prog8.code.core.DataType import prog8.code.core.Position import prog8.code.target.C64Target import prog8.code.target.Cx16Target import prog8.code.target.VMTarget import prog8tests.helpers.* class TestOptimization: FunSpec({ test("remove empty subroutine except start") { val sourcecode = """ main { sub start() { } sub empty() { ; going to be removed } } """ val result = compileText(C64Target(), true, sourcecode)!! val toplevelModule = result.compilerAst.toplevelModule val mainBlock = toplevelModule.statements.single() as Block val startSub = mainBlock.statements.single() as Subroutine result.compilerAst.entrypoint shouldBeSameInstanceAs startSub withClue("only start sub should remain") { startSub.name shouldBe "start" } withClue("compiler has inserted return in empty subroutines") { startSub.statements.single() shouldBe instanceOf() } } test("scan all asmsubs to see if another subroutine is referenced") { val src=""" main { sub foo() { cx16.r0++ } asmsub baz() { %asm{{ jsr p8s_foo }} } asmsub bar() { %asm{{ inx jmp p8s_foo }} } sub start() { bar() } }""" compileText(C64Target(), true, src) shouldNotBe null } test("don't remove empty subroutine if it's referenced") { val sourcecode = """ main { sub start() { uword xx = &empty xx++ } sub empty() { ; should not be removed } } """ val result = compileText(C64Target(), true, sourcecode)!! val toplevelModule = result.compilerAst.toplevelModule val mainBlock = toplevelModule.statements.single() as Block val startSub = mainBlock.statements[0] as Subroutine val emptySub = mainBlock.statements[1] as Subroutine result.compilerAst.entrypoint shouldBeSameInstanceAs startSub startSub.name shouldBe "start" emptySub.name shouldBe "empty" withClue("compiler has inserted return in empty subroutines") { emptySub.statements.single() shouldBe instanceOf() } } test("don't remove empty subroutine if it's referenced in vardecl") { val sourcecode = """ main { ubyte tw = other.width() sub start() { tw++ } } other { sub width() -> ubyte { cx16.r0++ return 80 } }""" compileText(C64Target(), true, sourcecode, writeAssembly = true) shouldNotBe null compileText(VMTarget(), true, sourcecode, writeAssembly = true) shouldNotBe null } test("generated constvalue from typecast inherits proper parent linkage") { val number = NumericLiteral(DataType.UBYTE, 11.0, Position.DUMMY) val tc = TypecastExpression(number, DataType.BYTE, false, Position.DUMMY) val program = Program("test", DummyFunctions, DummyMemsizer, DummyStringEncoder) tc.linkParents(ParentSentinel) tc.parent shouldNotBe null number.parent shouldNotBe null tc shouldBeSameInstanceAs number.parent val constvalue = tc.constValue(program)!! constvalue shouldBe instanceOf() constvalue.number shouldBe 11.0 constvalue.type shouldBe DataType.BYTE constvalue.parent shouldBeSameInstanceAs tc.parent } test("generated constvalue from prefixexpr inherits proper parent linkage") { val number = NumericLiteral(DataType.UBYTE, 11.0, Position.DUMMY) val pfx = PrefixExpression("-", number, Position.DUMMY) val program = Program("test", DummyFunctions, DummyMemsizer, DummyStringEncoder) pfx.linkParents(ParentSentinel) pfx.parent shouldNotBe null number.parent shouldNotBe null pfx shouldBeSameInstanceAs number.parent val constvalue = pfx.constValue(program)!! constvalue shouldBe instanceOf() constvalue.number shouldBe -11.0 constvalue.type shouldBe DataType.BYTE constvalue.parent shouldBeSameInstanceAs pfx.parent } test("various 'not' operator rewrites even without optimizations") { val src = """ main { sub start() { bool @shared a1 bool @shared a2 a1 = not a1 ; a1 = not a1 a1 = not not a1 ; a1 = a1, so removed totally a1 = not not not a1 ; a1 = not a1 a1 = not a1 or not a2 ; a1 = not (a1 and a2) a1 = not a1 and not a2 ; a1 = not (a1 or a2) } } """ val result = compileText(C64Target(), false, src, writeAssembly = true)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 9 val value1 = (stmts[4] as Assignment).value as PrefixExpression val value2 = (stmts[5] as Assignment).value as PrefixExpression val value3 = (stmts[6] as Assignment).value as PrefixExpression val value4 = (stmts[7] as Assignment).value as PrefixExpression value1.operator shouldBe "not" value2.operator shouldBe "not" value3.operator shouldBe "not" value4.operator shouldBe "not" value1.expression shouldBe instanceOf() value2.expression shouldBe instanceOf() (value3.expression as BinaryExpression).operator shouldBe "and" (value4.expression as BinaryExpression).operator shouldBe "or" } test("various 'not' operator rewrites with optimizations") { val src = """ main { sub start() { bool @shared a1 bool @shared a2 a1 = not a1 ; a1 = not a1 a1 = not not a1 ; a1 = a1, so removed totally a1 = not not not a1 ; a1 = not a1 a1 = not a1 or not a2 ; a1 = not (a1 and a2) a1 = not a1 and not a2 ; a1 = not (a1 or a2) } } """ val result = compileText(C64Target(), true, src, writeAssembly = true)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 9 val value1 = (stmts[4] as Assignment).value as PrefixExpression val value2 = (stmts[5] as Assignment).value as PrefixExpression val value3 = (stmts[6] as Assignment).value as PrefixExpression val value4 = (stmts[7] as Assignment).value as PrefixExpression value1.operator shouldBe "not" value2.operator shouldBe "not" value3.operator shouldBe "not" value4.operator shouldBe "not" value1.expression shouldBe instanceOf() value2.expression shouldBe instanceOf() (value3.expression as BinaryExpression).operator shouldBe "and" (value4.expression as BinaryExpression).operator shouldBe "or" } test("asmgen correctly deals with float typecasting in augmented assignment") { val src=""" %import floats main { sub start() { ubyte ub float ff = 1.0 ff += (ub as float) ; operator doesn't matter } } """ val result = compileText(C64Target(), optimize=false, src, writeAssembly = false)!! val assignFF = result.compilerAst.entrypoint.statements.last() as Assignment assignFF.isAugmentable shouldBe true assignFF.target.identifier!!.nameInSource shouldBe listOf("ff") val value = assignFF.value as BinaryExpression value.operator shouldBe "+" (value.left as? IdentifierReference)?.nameInSource shouldBe listOf("ff") value.right shouldBe instanceOf() compileText(C64Target(), optimize=false, src, writeAssembly = true) shouldNotBe null } test("unused variable removal") { val src=""" main { sub start() { ubyte unused ; removed ubyte @shared unused_but_shared ; this one should remain ubyte usedvar_only_written ; not removed because has multiple assignments usedvar_only_written=2 usedvar_only_written++ ubyte usedvar ; and this one remains too usedvar = msb(usedvar) } } """ val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! result.compilerAst.entrypoint.statements.size shouldBe 7 val alldecls = result.compilerAst.entrypoint.allDefinedSymbols.toList() alldecls.map { it.first } shouldBe listOf("unused_but_shared", "usedvar_only_written", "usedvar") } test("unused variable removal from subscope") { val src=""" main { sub start() { if cx16.r0!=0 { uword xx ; to be removed cx16.r0 = 0 } func2() sub func2() { uword yy ; to be removed yy=99 ; to be removed cx16.r0 = 0 rol(cx16.r0) } } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! result.compilerAst.entrypoint.statements.size shouldBe 3 val ifstmt = result.compilerAst.entrypoint.statements[0] as IfElse ifstmt.truepart.statements.size shouldBe 1 (ifstmt.truepart.statements[0] as Assignment).target.identifier!!.nameInSource shouldBe listOf("cx16", "r0") val func2 = result.compilerAst.entrypoint.statements[2] as Subroutine func2.statements.size shouldBe 2 (func2.statements[0] as Assignment).target.identifier!!.nameInSource shouldBe listOf("cx16", "r0") } test("unused subroutine removal") { val src=""" main { sub strip() { lstrip() } sub lstrip() { lstripped() } sub lstripped() { cx16.r0++ evenmore() cx16.r1++ } sub evenmore() { cx16.r1++ cx16.r2++ } sub start() { ; nothing } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! result.compilerAst.entrypoint.statements.size shouldBe 0 result.compilerAst.entrypoint.definingScope.statements.size shouldBe 1 } test("test simple augmented assignment optimization correctly initializes all variables") { val src=""" main { sub start() { ubyte @shared z1 z1 = 10 ubyte @shared z2 z2 = ~z2 bool @shared z3 z3 = not z3 uword @shared z4 z4 = (z4 as ubyte) ubyte @shared z5 z5 = z1+z5+5 ubyte @shared z6 z6 = z1+z6-5 } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! /* expected: ubyte z1 z1 = 10 ubyte z2 z2 = 255 bool z3 z3 = true uword z4 z4 = 0 ubyte z5 z5 = z1 + 5 ubyte z6 z6 = z1 - 5 */ val statements = result.compilerAst.entrypoint.statements statements.size shouldBe 12 val z1decl = statements[0] as VarDecl val z1init = statements[1] as Assignment val z2decl = statements[2] as VarDecl val z2init = statements[3] as Assignment val z3decl = statements[4] as VarDecl val z3init = statements[5] as Assignment val z4decl = statements[6] as VarDecl val z4init = statements[7] as Assignment val z5decl = statements[8] as VarDecl val z5init = statements[9] as Assignment val z6decl = statements[10] as VarDecl val z6init = statements[11] as Assignment z1decl.name shouldBe "z1" z1init.value shouldBe NumericLiteral(DataType.UBYTE, 10.0, Position.DUMMY) z2decl.name shouldBe "z2" z2init.value shouldBe NumericLiteral(DataType.UBYTE, 255.0, Position.DUMMY) z3decl.name shouldBe "z3" z3init.value shouldBe NumericLiteral(DataType.BOOL, 1.0, Position.DUMMY) z4decl.name shouldBe "z4" z4init.value shouldBe NumericLiteral(DataType.UWORD, 0.0, Position.DUMMY) z5decl.name shouldBe "z5" (z5init.value as BinaryExpression).operator shouldBe "+" (z5init.value as BinaryExpression).right shouldBe NumericLiteral(DataType.UBYTE, 5.0, Position.DUMMY) z6decl.name shouldBe "z6" (z6init.value as BinaryExpression).operator shouldBe "-" (z6init.value as BinaryExpression).right shouldBe NumericLiteral(DataType.UBYTE, 5.0, Position.DUMMY) } test("force_output option should work with optimizing memwrite assignment") { val src=""" main { %option force_output sub start() { uword @shared aa ubyte @shared zz @(aa) = zz + 32 } } """ val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 5 val assign=stmts.last() as Assignment (assign.target.memoryAddress?.addressExpression as IdentifierReference).nameInSource shouldBe listOf("aa") } test("don't optimize memory writes away") { val src=""" main { sub start() { uword @shared aa ubyte @shared zz @(aa) = zz + 32 ; do not optimize this away! } } """ val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 5 val assign=stmts.last() as Assignment (assign.target.memoryAddress?.addressExpression as IdentifierReference).nameInSource shouldBe listOf("aa") } test("correctly process constant prefix numbers") { val src=""" main { sub start() { ubyte @shared z1 = 1 ubyte @shared z2 = + 1 ubyte @shared z3 = ~ 1 bool @shared z4 = not 1 byte @shared z5 = - 1 } } """ val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 10 stmts.filterIsInstance().size shouldBe 5 stmts.filterIsInstance().size shouldBe 5 } test("correctly process constant prefix numbers with type mismatch and give error") { val src=""" main { sub start() { ubyte @shared z1 = - 200 } } """ val errors = ErrorReporterForTests() compileText(C64Target(), optimize=false, src, writeAssembly=false, errors = errors) shouldBe null errors.errors.size shouldBe 2 errors.errors[0] shouldContain "out of range" errors.errors[1] shouldContain "cannot assign word to byte" } test("out of range cast should give error") { val src=""" main { sub start() { ubyte @shared z1 = - 200 as ubyte } } """ val errors = ErrorReporterForTests() compileText(C64Target(), optimize=false, src, writeAssembly=false, errors = errors) shouldBe null errors.errors.size shouldBe 1 errors.errors[0] shouldContain "no cast" } test("test augmented expression asmgen") { val src = """ main { sub start() { ubyte c ubyte r ubyte q r = (q+r)-c q=r r = q+(r-c) q=r } }""" val result = compileText(C64Target(), optimize=false, src, writeAssembly=true)!! result.compilerAst.entrypoint.statements.size shouldBe 11 result.compilerAst.entrypoint.statements.last() shouldBe instanceOf() } test("keep the value initializer assignment if the next one depends on it") { val src=""" main { sub start() { uword @shared yy yy = 20 ; ok to remove =0 initializer before this uword @shared zz zz += 60 ; NOT ok to remove initializer, should evaluate to 60 ubyte @shared xx xx = 6+lsb(mkword(xx,22)) ; is not an initializer because it references xx } } """ val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! /* expected result: uword yy yy = 20 uword zz zz = 60 ubyte xx xx = 0 xx = abs(xx) xx += 6 */ val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 7 stmts.filterIsInstance().size shouldBe 3 stmts.filterIsInstance().size shouldBe 4 } test("only substitue assignments with 0 after a =0 initializer if it is the same variable") { val src=""" main { sub start() { uword @shared xx xx = xx + 20 ; is same var so can be changed just fine into xx=20 uword @shared yy xx = 20 yy = 0 ; is other var.. xx = xx+10 ; so this should not be changed into xx=10 } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! /* expected result: uword xx xx = 20 uword yy yy = 0 xx = 20 yy = 0 xx += 10 */ val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 7 stmts.filterIsInstance().size shouldBe 2 stmts.filterIsInstance().size shouldBe 5 val assignXX1 = stmts[1] as Assignment assignXX1.target.identifier!!.nameInSource shouldBe listOf("xx") assignXX1.value shouldBe NumericLiteral(DataType.UWORD, 20.0, Position.DUMMY) val assignXX2 = stmts.last() as Assignment assignXX2.target.identifier!!.nameInSource shouldBe listOf("xx") val xxValue = assignXX2.value as BinaryExpression xxValue.operator shouldBe "+" (xxValue.left as? IdentifierReference)?.nameInSource shouldBe listOf("xx") xxValue.right shouldBe NumericLiteral(DataType.UWORD, 10.0, Position.DUMMY) } test("multi-comparison with many values replaced by containment check on heap variable") { val src=""" main { sub start() { ubyte @shared source=99 ubyte @shared thingy=42 if source==3 or source==4 or source==99 or source==1 or source==2 or source==42 thingy++ } }""" compileText(VMTarget(), optimize=true, src, writeAssembly=true) shouldNotBe null val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! /* expected result: ubyte[] auto_heap_var = [1,2,3,4,42,99] ubyte source source = 99 ubyte thingy thingy = 42 if source in auto_heap_var thingy++ */ val stmts = result.compilerAst.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 6 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(2, Position.DUMMY), NumericLiteral.optimalInteger(3, Position.DUMMY), NumericLiteral.optimalInteger(4, Position.DUMMY), NumericLiteral.optimalInteger(42, Position.DUMMY), NumericLiteral.optimalInteger(99, Position.DUMMY)) } test("multi-comparison with few values replaced by inline containment check") { val src=""" main { sub start() { ubyte @shared source=99 ubyte @shared thingy=42 if source==3 or source==4 or source==99 thingy++ } }""" compileText(VMTarget(), optimize=true, src, writeAssembly=true) shouldNotBe null val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 5 val ifStmt = stmts[4] as IfElse val containment = ifStmt.condition as ContainmentCheck (containment.element as IdentifierReference).nameInSource shouldBe listOf("source") val array = (containment.iterable as ArrayLiteral) array.value.size shouldBe 3 array.value.map { (it as NumericLiteral).number } shouldBe listOf(3.0, 4.0, 99.0) } test("invalid multi-comparison (not all equals) not replaced") { val src=""" main { sub start() { ubyte @shared source=99 ubyte @shared thingy=42 if source==3 or source==4 or source!=99 or source==1 thingy++ } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 5 val ifStmt = stmts[4] as IfElse ifStmt.condition shouldBe instanceOf() } test("invalid multi-comparison (not all same needle) not replaced") { val src=""" main { sub start() { ubyte @shared source=99 ubyte @shared thingy=42 if source==3 or source==4 or thingy==99 or source==1 thingy++ } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 5 val ifStmt = stmts[4] as IfElse ifStmt.condition shouldBe instanceOf() } test("invalid multi-comparison (not all or) not replaced") { val src=""" main { sub start() { ubyte @shared source=99 ubyte @shared thingy=42 if source==3 or source==4 and source==99 or source==1 thingy++ } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 5 val ifStmt = stmts[4] as IfElse ifStmt.condition shouldBe instanceOf() } test("pointer indexing inside other expression ok") { val src=""" main{ sub start () { uword @shared eRef if eRef[3] & 10 ==0 { return } } }""" val result = compileText(C64Target(), optimize=true, src, writeAssembly=false)!! val stmts = result.compilerAst.entrypoint.statements stmts.size shouldBe 3 } test("repeated assignments to IO register should remain") { val srcX16=""" main { sub start() { ubyte @shared xx xx = 42 xx = 42 ; removed xx = 42 ; removed cx16.VERA_DATA0 = 0 cx16.VERA_DATA0 = 0 cx16.VERA_DATA0 = 0 @($9fff) = 0 @($9fff) = 0 @($9fff) = 0 return } }""" var result = compileText(Cx16Target(), true, srcX16, writeAssembly = true)!! var statements = result.compilerAst.entrypoint.statements statements.size shouldBe 9 (statements[1] as Assignment).target.identifier!!.nameInSource shouldBe listOf("xx") (statements[2] as Assignment).target.identifier!!.nameInSource shouldBe listOf("cx16", "VERA_DATA0") (statements[3] as Assignment).target.identifier!!.nameInSource shouldBe listOf("cx16", "VERA_DATA0") (statements[4] as Assignment).target.identifier!!.nameInSource shouldBe listOf("cx16", "VERA_DATA0") (statements[5] as Assignment).target.memoryAddress!!.addressExpression.constValue(result.compilerAst)!!.number shouldBe 0x9fff (statements[6] as Assignment).target.memoryAddress!!.addressExpression.constValue(result.compilerAst)!!.number shouldBe 0x9fff (statements[7] as Assignment).target.memoryAddress!!.addressExpression.constValue(result.compilerAst)!!.number shouldBe 0x9fff val srcC64=""" main { sub start() { ubyte @shared xx xx = 42 xx = 42 ;removed xx = 42 ;removed c64.EXTCOL = 0 c64.EXTCOL = 0 c64.EXTCOL = 0 @(53281) = 0 @(53281) = 0 @(53281) = 0 return } }""" result = compileText(C64Target(), true, srcC64, writeAssembly = true)!! statements = result.compilerAst.entrypoint.statements statements.size shouldBe 9 (statements[1] as Assignment).target.identifier!!.nameInSource shouldBe listOf("xx") (statements[2] as Assignment).target.identifier!!.nameInSource shouldBe listOf("c64", "EXTCOL") (statements[3] as Assignment).target.identifier!!.nameInSource shouldBe listOf("c64", "EXTCOL") (statements[4] as Assignment).target.identifier!!.nameInSource shouldBe listOf("c64", "EXTCOL") (statements[5] as Assignment).target.memoryAddress!!.addressExpression.constValue(result.compilerAst)!!.number shouldBe 53281.0 (statements[6] as Assignment).target.memoryAddress!!.addressExpression.constValue(result.compilerAst)!!.number shouldBe 53281.0 (statements[7] as Assignment).target.memoryAddress!!.addressExpression.constValue(result.compilerAst)!!.number shouldBe 53281.0 } test("no string error when inlining") { val text=""" main { sub start() { test() } sub test() { cx16.r0 = "abc" } }""" compileText(C64Target(), true, text, writeAssembly = false) shouldNotBe null } test("replacing string print by chrout with referenced string elsewhere shouldn't give string symbol error") { val text=""" %import textio main { sub start() { str key = "test" txt.print(":") if key != ":" { cx16.r0++ } } } """ compileText(VMTarget(), true, text, writeAssembly = false) shouldNotBe null } test("sub only called by asm should not be optimized away") { val src=""" main { sub start() { %asm{{ jsr p8s_test }} } sub test() { cx16.r0++ } }""" compileText(Cx16Target(), true, src, writeAssembly = true) shouldNotBe null } test("no crash for making var in removed/inlined subroutine fully scoped") { val src=""" main { sub start() { test() } sub test() { sub nested() { ubyte counter counter++ } test2(main.test.nested.counter) } sub test2(ubyte value) { value++ } }""" val errors = ErrorReporterForTests() compileText(Cx16Target(), true, src, writeAssembly = false, errors = errors) shouldNotBe null } test("var to const") { val src=""" main { sub start() { ubyte xx=10 ; to const ubyte @shared yy=20 ; remain var cx16.r0L = xx+yy } }""" val errors = ErrorReporterForTests() val result = compileText(Cx16Target(), true, src, writeAssembly = false, errors = errors)!! val st = result.compilerAst.entrypoint.statements st.size shouldBe 4 val xxConst = st[0] as VarDecl xxConst.type shouldBe VarDeclType.CONST xxConst.name shouldBe "xx" (xxConst.value as? NumericLiteral)?.number shouldBe 10.0 (st[1] as VarDecl).type shouldBe VarDeclType.VAR val expr = (st[3] as Assignment).value as BinaryExpression (expr.left as? IdentifierReference)?.nameInSource shouldBe listOf("yy") (expr.right as? NumericLiteral)?.number shouldBe 10.0 } test("var to const inside typecast") { val src=""" main { uword variable ; will be made a const sub start() { cx16.r0 = msb(variable) as uword } }""" compileText(VMTarget(), true, src, writeAssembly = false) shouldNotBe null } test("De Morgan's laws") { val src=""" main { sub start() { bool @shared a1 bool @shared a2 if not a1 or not a2 cx16.r0++ if not a1 and not a2 cx16.r0++ if cx16.r0L==42 or not a2 cx16.r0L++ if not a2 or cx16.r0L==42 cx16.r0L++ } }""" val result = compileText(Cx16Target(), true, src, writeAssembly = false)!! val st = result.compilerAst.entrypoint.statements st.size shouldBe 8 val if1c = (st[4] as IfElse).condition as PrefixExpression val if2c = (st[5] as IfElse).condition as PrefixExpression val if3c = (st[6] as IfElse).condition as PrefixExpression val if4c = (st[7] as IfElse).condition as PrefixExpression if1c.operator shouldBe "not" if2c.operator shouldBe "not" if3c.operator shouldBe "not" if4c.operator shouldBe "not" (if1c.expression as BinaryExpression).operator shouldBe "and" (if2c.expression as BinaryExpression).operator shouldBe "or" (if3c.expression as BinaryExpression).operator shouldBe "and" (if4c.expression as BinaryExpression).operator shouldBe "and" } test("absorption laws") { val src=""" main { sub start() { bool @shared a bool @shared b if a or (a and b) cx16.r0 ++ if a or (b and a) cx16.r0 ++ if a and (a or b) cx16.r0 ++ if a and (b or a) cx16.r0 ++ if a and (b and a) cx16.r0 ++ if a or (b or a) cx16.r0 ++ if (b and a) and b cx16.r0 ++ if (b or a) or b cx16.r0 ++ } }""" val result = compileText(Cx16Target(), true, src, writeAssembly = false)!! val st = result.compilerAst.entrypoint.statements st.size shouldBe 12 val if1 = st[4] as IfElse val if2 = st[5] as IfElse val if3 = st[6] as IfElse val if4 = st[7] as IfElse (if1.condition as IdentifierReference).nameInSource shouldBe listOf("a") (if2.condition as IdentifierReference).nameInSource shouldBe listOf("a") (if3.condition as IdentifierReference).nameInSource shouldBe listOf("a") (if4.condition as IdentifierReference).nameInSource shouldBe listOf("a") val if5 = st[8] as IfElse val if6 = st[9] as IfElse val if7 = st[10] as IfElse val if8 = st[11] as IfElse val if5bc = if5.condition as BinaryExpression val if6bc = if6.condition as BinaryExpression val if7bc = if7.condition as BinaryExpression val if8bc = if8.condition as BinaryExpression if5bc.left shouldBe instanceOf() if5bc.right shouldBe instanceOf() if6bc.left shouldBe instanceOf() if6bc.right shouldBe instanceOf() if7bc.left shouldBe instanceOf() if7bc.right shouldBe instanceOf() if8bc.left shouldBe instanceOf() if8bc.right shouldBe instanceOf() } test("funky bitshifts") { val src=""" main { sub start() { const uword one = 1 const uword two = 2 uword @shared answer = one * two >> 8 funcw(one * two >> 8) const uword uw1 = 99 const uword uw2 = 22 uword @shared answer2 = uw1 * uw2 >> 8 ; optimized into msb(uw1*uw2) as uword funcw(uw1 * uw2 >> 8) uword @shared uw3 = 99 uword @shared uw4 = 22 uword @shared answer3 = uw3 * uw4 >> 8 ; optimized into msb(uw1*uw2) as uword funcw(uw3 * uw4 >> 8) } sub funcw(uword ww) { cx16.r0++ } }""" val result = compileText(Cx16Target(), true, src, writeAssembly = false)!! val st = result.compilerAst.entrypoint.statements st.size shouldBe 17 val answerValue = (st[3] as Assignment).value answerValue shouldBe NumericLiteral(DataType.UWORD, 0.0, Position.DUMMY) val funcarg1 = (st[4] as FunctionCallStatement).args.single() funcarg1 shouldBe NumericLiteral(DataType.UWORD, 0.0, Position.DUMMY) val answer2Value = (st[8] as Assignment).value answer2Value shouldBe NumericLiteral(DataType.UWORD, 8.0, Position.DUMMY) val funcarg2 = (st[9] as FunctionCallStatement).args.single() funcarg2 shouldBe NumericLiteral(DataType.UWORD, 8.0, Position.DUMMY) val answer3ValueTc = (st[15] as Assignment).value as TypecastExpression answer3ValueTc.type shouldBe DataType.UWORD val answer3Value = answer3ValueTc.expression as FunctionCallExpression answer3Value.target.nameInSource shouldBe listOf("msb") answer3Value.args.single() shouldBe instanceOf() val funcarg3tc = (st[16] as FunctionCallStatement).args.single() as TypecastExpression funcarg3tc.type shouldBe DataType.UWORD val funcarg3 = funcarg3tc.expression as FunctionCallExpression funcarg3.target.nameInSource shouldBe listOf("msb") funcarg3.args.single() shouldBe instanceOf() } test("no operand swap on logical expressions with shortcircuit evaluation") { val src=""" %import diskio %zeropage basicsafe %option no_sysinit main { str scanline_buf = "?"* 20 sub start() { if diskio.f_open("test.prg") and diskio.f_read(scanline_buf, 2)==2 cx16.r0++ if diskio.f_open("test.prg") or diskio.f_read(scanline_buf, 2)==2 cx16.r0++ if diskio.f_open("test.prg") xor diskio.f_read(scanline_buf, 2)==2 cx16.r0++ } }""" val result = compileText(Cx16Target(), true, src, writeAssembly = false)!! val st = result.compilerAst.entrypoint.statements st.size shouldBe 3 val ifCond1 = (st[0] as IfElse).condition as BinaryExpression val ifCond2 = (st[1] as IfElse).condition as BinaryExpression val ifCond3 = (st[2] as IfElse).condition as BinaryExpression (ifCond1.left as FunctionCallExpression).target.nameInSource shouldBe listOf("diskio", "f_open") (ifCond2.left as FunctionCallExpression).target.nameInSource shouldBe listOf("diskio", "f_open") (ifCond3.left as FunctionCallExpression).target.nameInSource shouldBe listOf("diskio", "f_open") val right1 = ifCond1.right as BinaryExpression val right2 = ifCond2.right as BinaryExpression val right3 = ifCond3.right as BinaryExpression (right1.left as FunctionCallExpression).target.nameInSource shouldBe listOf("diskio", "f_read") (right2.left as FunctionCallExpression).target.nameInSource shouldBe listOf("diskio", "f_read") (right3.left as FunctionCallExpression).target.nameInSource shouldBe listOf("diskio", "f_read") } test("eliminate same target register assignments") { val src=""" %zeropage basicsafe %option no_sysinit main { romsub ${'$'}2000 = func1() clobbers(X) -> ubyte @A, word @R0, byte @R1 romsub ${'$'}3000 = func2() clobbers(X) -> ubyte @A, uword @R0, uword @R1 romsub ${'$'}4000 = func3() clobbers(X) -> ubyte @R0 sub start() { bool flag void cbm.GETIN() flag, cx16.r1L = cbm.GETIN() void, cx16.r0s, cx16.r1sL = func1() void, cx16.r2, cx16.r1 = func2() cx16.r0L = func3() cx16.r0H = func3() } }""" val result = compileText(C64Target(), true, src, writeAssembly = true)!! val st = result.codegenAst!!.entrypoint()!!.children st.size shouldBe 9 (st[2] as PtFunctionCall).name shouldBe "cbm.GETIN" (st[2] as PtFunctionCall).void shouldBe true val a1 = st[3] as PtAssignment (a1.value as PtFunctionCall).name shouldBe "cbm.GETIN" a1.multiTarget shouldBe true a1.children.size shouldBe 3 (a1.children[0] as PtAssignTarget).void shouldBe false (a1.children[0] as PtAssignTarget).identifier!!.name shouldBe "p8b_main.p8s_start.p8v_flag" (a1.children[1] as PtAssignTarget).void shouldBe false (a1.children[1] as PtAssignTarget).identifier!!.name shouldBe "cx16.r1L" (st[4] as PtFunctionCall).name shouldBe "p8b_main.p8s_func1" (st[4] as PtFunctionCall).void shouldBe true val a2 = st[5] as PtAssignment (a2.value as PtFunctionCall).name shouldBe "p8b_main.p8s_func2" a2.multiTarget shouldBe true a2.children.size shouldBe 4 (a2.children[0] as PtAssignTarget).void shouldBe true (a2.children[1] as PtAssignTarget).void shouldBe false (a2.children[1] as PtAssignTarget).identifier!!.name shouldBe "cx16.r2" (a2.children[2] as PtAssignTarget).void shouldBe true (st[6] as PtFunctionCall).name shouldBe "p8b_main.p8s_func3" (st[6] as PtFunctionCall).void shouldBe true val a3 = st[7] as PtAssignment (a3.value as PtFunctionCall).name shouldBe "p8b_main.p8s_func3" a3.multiTarget shouldBe false a3.children.size shouldBe 2 (a3.children[0] as PtAssignTarget).void shouldBe false (a3.children[0] as PtAssignTarget).identifier!!.name shouldBe "cx16.r0H" } test("symbol table correct in asmgen after earlier optimization steps") { val src=""" main { sub start() { uword @shared bulletRef, enemyRef const ubyte BD_Y = 10 const ubyte EN_Y = 11 if bulletRef[BD_Y] == enemyRef[EN_Y] or bulletRef[BD_Y] == enemyRef[EN_Y] + 1 { cx16.r0++ } } }""" compileText(VMTarget(), true, src, writeAssembly = true) shouldNotBe null compileText(C64Target(), true, src, writeAssembly = true) shouldNotBe null } test("optimizing inlined functions must reference proper scopes") { val src=""" main { sub start() { void other.sub1() cx16.r0L = other.sub1()+other.sub1() } } other { sub sub2() -> ubyte{ cx16.r0++ cx16.r1++ return cx16.r0L } sub sub1() -> ubyte { return sub2() } }""" compileText(VMTarget(), true, src, writeAssembly = true) shouldNotBe null compileText(C64Target(), true, src, writeAssembly = true) shouldNotBe null } })