prog8/compiler/test/TestOptimization.kt
2024-09-11 03:24:30 +02:00

1107 lines
40 KiB
Kotlin

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<Return>()
}
}
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<Return>()
}
}
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<NumericLiteral>()
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<NumericLiteral>()
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<IdentifierReference>()
value2.expression shouldBe instanceOf<IdentifierReference>()
(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<IdentifierReference>()
value2.expression shouldBe instanceOf<IdentifierReference>()
(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<TypecastExpression>()
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<VarDecl>().size shouldBe 5
stmts.filterIsInstance<Assignment>().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<Return>()
}
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<VarDecl>().size shouldBe 3
stmts.filterIsInstance<Assignment>().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<VarDecl>().size shouldBe 2
stmts.filterIsInstance<Assignment>().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<BinaryExpression>()
}
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<BinaryExpression>()
}
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<BinaryExpression>()
}
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<IdentifierReference>()
if5bc.right shouldBe instanceOf<IdentifierReference>()
if6bc.left shouldBe instanceOf<IdentifierReference>()
if6bc.right shouldBe instanceOf<IdentifierReference>()
if7bc.left shouldBe instanceOf<IdentifierReference>()
if7bc.right shouldBe instanceOf<IdentifierReference>()
if8bc.left shouldBe instanceOf<IdentifierReference>()
if8bc.right shouldBe instanceOf<IdentifierReference>()
}
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<BinaryExpression>()
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<BinaryExpression>()
}
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
}
})