mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
1262 lines
46 KiB
Kotlin
1262 lines
46 KiB
Kotlin
package prog8tests.compiler
|
|
|
|
import io.kotest.assertions.fail
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.engine.spec.tempdir
|
|
import io.kotest.matchers.collections.shouldBeEmpty
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.shouldNotBe
|
|
import io.kotest.matchers.string.shouldContain
|
|
import io.kotest.matchers.types.instanceOf
|
|
import prog8.ast.*
|
|
import prog8.ast.expressions.*
|
|
import prog8.ast.statements.*
|
|
import prog8.ast.walk.IAstVisitor
|
|
import prog8.code.ast.*
|
|
import prog8.code.target.C64Target
|
|
import prog8.code.target.Cx16Target
|
|
import prog8.code.target.Qemu68kTarget
|
|
import prog8.code.target.VMTarget
|
|
import prog8.intermediate.IRFileReader
|
|
import prog8.vm.VmRunner
|
|
import prog8.vm.VmVariableAllocator
|
|
import prog8tests.helpers.ErrorReporterForTests
|
|
import prog8tests.helpers.compileText
|
|
import kotlin.io.path.readText
|
|
|
|
|
|
class TestOptimization2: FunSpec({
|
|
val outputDir = tempdir().toPath()
|
|
|
|
test("boolean comparisons without optimization can be assembled") {
|
|
val src="""
|
|
main {
|
|
sub start() {
|
|
bool @shared pre_start, xxx
|
|
|
|
if (pre_start != false and xxx) {
|
|
return
|
|
} else if (pre_start != false and xxx) {
|
|
return
|
|
}
|
|
}
|
|
}"""
|
|
compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
|
|
}
|
|
|
|
test("correct unused block removal for virtual target") {
|
|
val src="""
|
|
main {
|
|
sub start() {
|
|
cx16.r0++
|
|
}
|
|
}
|
|
|
|
some_block {
|
|
pointer buffer = memory("arena", 2000, 0)
|
|
}
|
|
|
|
|
|
other_block {
|
|
sub redherring (uword buffer) {
|
|
%ir {{
|
|
loadm.w r99000.w,[other_block.redherring.buffer]
|
|
}}
|
|
}
|
|
}
|
|
"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
|
|
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
|
|
VmRunner().runProgram(virtfile.readText(), false)
|
|
}
|
|
|
|
test("correct unused block removal for c64 target") {
|
|
val src="""
|
|
main {
|
|
sub start() {
|
|
cx16.r0++
|
|
}
|
|
}
|
|
|
|
some_block {
|
|
uword buffer = memory("arena", 2000, 0)
|
|
}
|
|
|
|
|
|
other_block {
|
|
sub redherring (uword buffer) {
|
|
%asm {{
|
|
lda #<p8b_other_block.p8s_redherring.p8v_buffer
|
|
ldy #>p8b_other_block.p8s_redherring.p8v_buffer
|
|
}}
|
|
}
|
|
}"""
|
|
compileText(C64Target(), true, src, outputDir) shouldNotBe null
|
|
}
|
|
|
|
test("complicated if statement optimization") {
|
|
val src="""
|
|
main {
|
|
sub start() {
|
|
bool @shared ans
|
|
ans = false
|
|
if (ans == true) {
|
|
return
|
|
} else {
|
|
goto done
|
|
}
|
|
done:
|
|
return
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val st = result.compilerAst.entrypoint.statements
|
|
st.size shouldBe 6
|
|
val ifelseCond = (st[2] as IfElse).condition as PrefixExpression
|
|
ifelseCond.operator shouldBe "not"
|
|
(ifelseCond.expression as IdentifierReference).nameInSource shouldBe listOf("ans")
|
|
}
|
|
|
|
test("inline multi-value returns in void statement context") {
|
|
// Tests that subroutines with multi-value returns are inlined when called with 'void'
|
|
// The void calls should be removed entirely if return values are simple (no side effects)
|
|
val src="""
|
|
main {
|
|
ubyte @shared v1 = 10
|
|
ubyte @shared v2 = 20
|
|
ubyte @shared v3 = 30
|
|
|
|
sub start() {
|
|
ubyte @shared tmp
|
|
|
|
; These void calls should be removed by the inliner
|
|
void get_single()
|
|
void multi_literals()
|
|
void multi_vars()
|
|
|
|
; These expression calls should remain (but may be inlined)
|
|
tmp = get_single()
|
|
tmp, tmp = multi_literals()
|
|
tmp, tmp, tmp = multi_vars()
|
|
}
|
|
|
|
sub get_single() -> ubyte {
|
|
return 42
|
|
}
|
|
|
|
sub multi_literals() -> ubyte, ubyte {
|
|
return 10, 20
|
|
}
|
|
|
|
sub multi_vars() -> ubyte, ubyte, ubyte {
|
|
return v1, v2, v3
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val mainBlock = result.compilerAst.entrypoint.definingBlock
|
|
val startSub = mainBlock.statements.find { it is Subroutine && it.name == "start" }!! as Subroutine
|
|
|
|
// Count FunctionCallStatement nodes in start() - the void calls should be removed
|
|
// Only the expression context calls should remain
|
|
val voidCalls = startSub.statements.filterIsInstance<FunctionCallStatement>()
|
|
|
|
// All 3 void calls should have been removed
|
|
voidCalls.size shouldBe 0
|
|
}
|
|
|
|
test("multi-value returns inlined in expression context") {
|
|
// Tests that multi-value returns ARE inlined when values are captured (for simple cases)
|
|
val src="""
|
|
main {
|
|
ubyte @shared gv1 = 10
|
|
ubyte @shared gv2 = 20
|
|
|
|
sub start() {
|
|
ubyte result_a, result_b
|
|
result_a, result_b = get_globals()
|
|
cx16.r0 = result_a + result_b ; use the variables to prevent removal
|
|
}
|
|
|
|
sub get_globals() -> ubyte, ubyte {
|
|
return gv1, gv2
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val mainBlock = result.compilerAst.entrypoint.definingBlock
|
|
val startSub = mainBlock.statements.find { it is Subroutine && it.name == "start" }!! as Subroutine
|
|
val stmts = startSub.statements
|
|
|
|
// Multi-value assignment should be split into separate assignments (no multi-target)
|
|
val multiAssigns = stmts.filterIsInstance<Assignment>().filter {
|
|
it.target.multi?.isNotEmpty() == true
|
|
}
|
|
multiAssigns.size shouldBe 0
|
|
|
|
// Should have separate single assignments for result_a and result_b (not the cx16.r0 assignment)
|
|
val resultAssigns = stmts.filterIsInstance<Assignment>().filter {
|
|
it.target.identifier?.nameInSource?.lastOrNull() in listOf("result_a", "result_b")
|
|
}
|
|
resultAssigns.size shouldBe 2
|
|
|
|
// Check the values are identifier references (to gv1 and gv2)
|
|
val idAssigns = resultAssigns.filter { it.value is IdentifierReference }
|
|
idAssigns.size shouldBe 2
|
|
}
|
|
|
|
test("inline zero-return void calls are removed") {
|
|
// Tests that subroutines with empty return statements are inlined when called with 'void'
|
|
// Only safe when there are no side effects in the subroutine body
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void empty_return()
|
|
cx16.r0++
|
|
}
|
|
sub empty_return() {
|
|
return
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Void call should be removed entirely (no side effects)
|
|
val voidCalls = startSub.statements.filterIsInstance<FunctionCallStatement>()
|
|
voidCalls.size shouldBe 0
|
|
}
|
|
|
|
test("void calls with side effects preserve the side effects through inlining") {
|
|
// Tests that void calls containing function calls (side effects) preserve the side effects
|
|
// The inliner recursively inlines the entire call chain, preserving all side effects
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void with_side_effect()
|
|
cx16.r0++
|
|
}
|
|
sub with_side_effect() {
|
|
helper() ; this is a side effect
|
|
return
|
|
}
|
|
sub helper() {
|
|
cx16.r1++
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// The void call is inlined along with helper(), and the side effect (cx16.r1++) is preserved
|
|
// Check that cx16.r1++ appears in the start() subroutine (proving side effect was preserved)
|
|
val hasSideEffect = startSub.statements.any { stmt ->
|
|
if (stmt is Assignment) {
|
|
stmt.target.identifier?.nameInSource?.lastOrNull() == "r1" &&
|
|
stmt.value is BinaryExpression
|
|
} else false
|
|
}
|
|
hasSideEffect shouldBe true
|
|
}
|
|
|
|
test("inline single-value returns in expression context") {
|
|
// Tests that single-value returns are inlined in expression context (parameterless subs only)
|
|
val src = """
|
|
main {
|
|
ubyte @shared gv = 100
|
|
|
|
sub start() {
|
|
ubyte result
|
|
result = get_global()
|
|
cx16.r0 = result + gv ; use result to prevent optimization
|
|
}
|
|
sub get_global() -> ubyte {
|
|
return gv
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val mainBlock = result.compilerAst.entrypoint.definingBlock
|
|
val startSub = mainBlock.statements.filterIsInstance<Subroutine>().find { it.name == "start" }!!
|
|
val stmts = startSub.statements
|
|
|
|
// Should have single assignment with identifier reference (gv), not function call
|
|
val assigns = stmts.filterIsInstance<Assignment>()
|
|
.filter { it.target.identifier?.nameInSource?.lastOrNull() == "result" }
|
|
assigns.size shouldBe 1
|
|
assigns[0].value shouldBe instanceOf<IdentifierReference>()
|
|
}
|
|
|
|
test("inline three-value returns in expression context") {
|
|
// Tests that three-value returns are inlined when values are captured
|
|
val src = """
|
|
main {
|
|
ubyte @shared gv1 = 10
|
|
ubyte @shared gv2 = 20
|
|
ubyte @shared gv3 = 30
|
|
|
|
sub start() {
|
|
ubyte result_a, result_b, result_c
|
|
result_a, result_b, result_c = get_globals()
|
|
cx16.r0 = result_a + result_b + result_c
|
|
}
|
|
sub get_globals() -> ubyte, ubyte, ubyte {
|
|
return gv1, gv2, gv3
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val mainBlock = result.compilerAst.entrypoint.definingBlock
|
|
val startSub = mainBlock.statements.filterIsInstance<Subroutine>().find { it.name == "start" }!!
|
|
val stmts = startSub.statements
|
|
|
|
// Multi-assignment should be split into separate assignments (no multi-target)
|
|
val multiAssigns = stmts.filterIsInstance<Assignment>()
|
|
.filter { it.target.multi?.isNotEmpty() == true }
|
|
multiAssigns.size shouldBe 0
|
|
|
|
// Should have 3 separate single assignments
|
|
val resultAssigns = stmts.filterIsInstance<Assignment>()
|
|
.filter { it.target.identifier?.nameInSource?.lastOrNull() in listOf("result_a", "result_b", "result_c") }
|
|
resultAssigns.size shouldBe 3
|
|
|
|
// All should be identifier references
|
|
val idAssigns = resultAssigns.filter { it.value is IdentifierReference }
|
|
idAssigns.size shouldBe 3
|
|
}
|
|
|
|
test("inline void call with one parameter") {
|
|
// Tests that void calls with one parameter are inlined when args are simple
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void take_one(1)
|
|
cx16.r0++
|
|
}
|
|
sub take_one(ubyte p) {
|
|
return
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Void call should be removed entirely
|
|
val hasVoidCall = startSub.statements.any { stmt ->
|
|
stmt is FunctionCallStatement &&
|
|
stmt.target.nameInSource.last() == "take_one" &&
|
|
stmt.void
|
|
}
|
|
hasVoidCall shouldBe false
|
|
}
|
|
|
|
test("poke with indexed pointer address is not rewritten to plain assignment") {
|
|
// pokel(&&arena[1], x) --> arena=x is only valid for a plain variable address;
|
|
// indexed/dereferenced addresses must keep their index (used to miscompile
|
|
// implicit indexed-base struct field writes such as arena[1].s = X).
|
|
val src = """
|
|
main {
|
|
struct Node {
|
|
^^uword s
|
|
}
|
|
sub start() {
|
|
uword[4] backing
|
|
^^Node arena = memory("arena", 16, 0)
|
|
arena[1].s = &backing
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// the pokel call must have survived the optimizer
|
|
val pokes = startSub.statements.filterIsInstance<FunctionCallStatement>()
|
|
.filter { it.target.nameInSource.single() == "pokel" }
|
|
pokes.size shouldBe 1
|
|
}
|
|
|
|
xtest("inline void call with two parameters") {
|
|
// Tests that void calls with two parameters are inlined when args are simple
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void take_two(1, 2)
|
|
cx16.r0++
|
|
}
|
|
sub take_two(ubyte a, ubyte b) {
|
|
return
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Void call should be removed entirely
|
|
val hasVoidCall = startSub.statements.any { stmt ->
|
|
stmt is FunctionCallStatement &&
|
|
stmt.target.nameInSource.last() == "take_two" &&
|
|
stmt.void
|
|
}
|
|
hasVoidCall shouldBe false
|
|
}
|
|
|
|
test("inline call with one return value and one parameter") {
|
|
// Tests that function calls returning one value with one parameter are inlined
|
|
// and that the parameter is correctly substituted with the argument
|
|
val src = """
|
|
main {
|
|
ubyte @shared gv = 100
|
|
|
|
sub start() {
|
|
ubyte result
|
|
result = get_value(42)
|
|
cx16.r0 = result + gv
|
|
}
|
|
sub get_value(ubyte x) -> ubyte {
|
|
return gv + x
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Should have assignment with binary expression (parameter substituted with literal 42)
|
|
val assigns = startSub.statements.filterIsInstance<Assignment>()
|
|
.filter { it.target.identifier?.nameInSource?.lastOrNull() == "result" }
|
|
assigns.size shouldBe 1
|
|
val value = assigns[0].value
|
|
value shouldBe instanceOf<BinaryExpression>()
|
|
// Verify the parameter x was replaced with argument 42
|
|
val binExpr = value as BinaryExpression
|
|
binExpr.right shouldBe instanceOf<NumericLiteral>()
|
|
}
|
|
|
|
xtest("inline call with two return values and two parameters") {
|
|
// Tests that function calls returning two values with two parameters are inlined
|
|
// and that both parameters are correctly substituted
|
|
val src = """
|
|
main {
|
|
ubyte @shared v1 = 10
|
|
ubyte @shared v2 = 20
|
|
|
|
sub start() {
|
|
ubyte a, b
|
|
a, b = get_two(1, 2)
|
|
cx16.r0 = a + b
|
|
}
|
|
sub get_two(ubyte x, ubyte y) -> ubyte, ubyte {
|
|
return v1 + x, v2 + y
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
val stmts = startSub.statements
|
|
|
|
// Multi-assignment should be split into separate assignments
|
|
val multiAssigns = stmts.filterIsInstance<Assignment>()
|
|
.filter { it.target.multi?.isNotEmpty() == true }
|
|
multiAssigns.size shouldBe 0
|
|
|
|
// Should have 2 separate single assignments
|
|
val resultAssigns = stmts.filterIsInstance<Assignment>()
|
|
.filter { it.target.identifier?.nameInSource?.lastOrNull() in listOf("a", "b") }
|
|
resultAssigns.size shouldBe 2
|
|
|
|
// All should be binary expressions (parameter substitution occurred)
|
|
val binAssigns = resultAssigns.filter { it.value is BinaryExpression }
|
|
binAssigns.size shouldBe 2
|
|
|
|
// Verify parameters were replaced with arguments (1 and 2)
|
|
// The right side of each binary expression should be a NumericLiteral
|
|
binAssigns.forEach { assign ->
|
|
val binExpr = assign.value as BinaryExpression
|
|
binExpr.right shouldBe instanceOf<NumericLiteral>()
|
|
}
|
|
}
|
|
|
|
test("call with two parameters where one has a register alias is not inlined") {
|
|
// Tests that a function call with two parameters where one has a register alias is NOT inlined
|
|
val src = """
|
|
main {
|
|
ubyte @shared v1 = 10
|
|
ubyte @shared v2 = 20
|
|
|
|
sub start() {
|
|
ubyte a
|
|
a = get_two(1, 2)
|
|
cx16.r0 = a
|
|
}
|
|
sub get_two(ubyte x, ubyte y @R0) -> ubyte {
|
|
return v1
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
val stmts = startSub.statements
|
|
|
|
// Should NOT be inlined, so the FunctionCallExpression should still exist in start()
|
|
val callNodes = stmts.filter { stmt ->
|
|
if (stmt is Assignment) stmt.value is FunctionCallExpression && (stmt.value as FunctionCallExpression).target.nameInSource.last() == "get_two"
|
|
else false
|
|
}
|
|
|
|
// There should be exactly one call remaining
|
|
callNodes.size shouldBe 1
|
|
}
|
|
|
|
xtest("inline void call with six parameters") {
|
|
// Tests that void calls with six parameters are inlined when args are simple
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void take_six(1, 2, 3, 4, 5, 6)
|
|
cx16.r0++
|
|
}
|
|
sub take_six(ubyte a, ubyte b, ubyte c, ubyte d, ubyte e, ubyte f) {
|
|
return
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Void call should be removed entirely
|
|
val hasVoidCall = startSub.statements.any { stmt ->
|
|
stmt is FunctionCallStatement &&
|
|
stmt.target.nameInSource.last() == "take_six" &&
|
|
stmt.void
|
|
}
|
|
hasVoidCall shouldBe false
|
|
}
|
|
|
|
test("inline two-param single-return is inlined on m68k") {
|
|
val src = """
|
|
main {
|
|
uword @shared result
|
|
sub start() {
|
|
result = cellidx(10, 20)
|
|
}
|
|
inline sub cellidx(ubyte cx, ubyte cy) -> uword {
|
|
return cx + 60*cy
|
|
}
|
|
}"""
|
|
val result = compileText(Qemu68kTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
val hasCall = startSub.statements.any { stmt ->
|
|
(stmt as? Assignment)?.value.let { it is FunctionCallExpression && it.target.nameInSource.last() == "cellidx" }
|
|
}
|
|
hasCall shouldBe false
|
|
val assigns = startSub.statements.filterIsInstance<Assignment>()
|
|
.filter { it.target.identifier?.nameInSource?.lastOrNull() == "result" }
|
|
assigns.size shouldBe 1
|
|
// Inlined 10 + 60*20 is constant-folded to 1210; either shape proves the call is gone.
|
|
val folded = (assigns[0].value as? NumericLiteral)?.number == 1210.0
|
|
(assigns[0].value is BinaryExpression || folded) shouldBe true
|
|
}
|
|
|
|
test("inline two-param single-return is NOT inlined on 6502") {
|
|
val src = """
|
|
main {
|
|
uword @shared result
|
|
sub start() {
|
|
result = cellidx(10, 20)
|
|
}
|
|
inline sub cellidx(ubyte cx, ubyte cy) -> uword {
|
|
return cx + 60*cy
|
|
}
|
|
}"""
|
|
val result = compileText(Cx16Target(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
val hasCall = startSub.statements.any { stmt ->
|
|
(stmt as? Assignment)?.value.let { it is FunctionCallExpression && it.target.nameInSource.last() == "cellidx" }
|
|
}
|
|
hasCall shouldBe true
|
|
}
|
|
|
|
test("inline two-param call with complex arg is NOT inlined on m68k") {
|
|
val src = """
|
|
main {
|
|
uword @shared result
|
|
ubyte @shared a
|
|
ubyte @shared b
|
|
sub start() {
|
|
result = cellidx(a+b, 20)
|
|
}
|
|
inline sub cellidx(ubyte cx, ubyte cy) -> uword {
|
|
return cx + 60*cy
|
|
}
|
|
}"""
|
|
val result = compileText(Qemu68kTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
val hasCall = startSub.statements.any { stmt ->
|
|
(stmt as? Assignment)?.value.let { it is FunctionCallExpression && it.target.nameInSource.last() == "cellidx" }
|
|
}
|
|
hasCall shouldBe true
|
|
}
|
|
|
|
test("inline two-param call with too-complex body is NOT inlined on m68k") {
|
|
val src = """
|
|
main {
|
|
uword @shared result
|
|
sub start() {
|
|
result = cellidx(10, 20)
|
|
}
|
|
sub helper(ubyte v) -> uword {
|
|
return v
|
|
}
|
|
inline sub cellidx(ubyte cx, ubyte cy) -> uword {
|
|
return helper(cx) + cy
|
|
}
|
|
}"""
|
|
val result = compileText(Qemu68kTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
val hasCall = startSub.statements.any { stmt ->
|
|
(stmt as? Assignment)?.value.let { it is FunctionCallExpression && it.target.nameInSource.last() == "cellidx" }
|
|
}
|
|
hasCall shouldBe true
|
|
}
|
|
|
|
test("parameterized subroutine with 'inline' is inlined (1 parameter)") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
foo(1)
|
|
}
|
|
inline sub foo(ubyte x) {
|
|
cx16.r0 = x
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Should no longer contain a function call to foo
|
|
val hasFooCall = startSub.statements.any { stmt ->
|
|
stmt is FunctionCallStatement &&
|
|
stmt.target.nameInSource.last() == "foo"
|
|
}
|
|
hasFooCall shouldBe false
|
|
}
|
|
|
|
test("parameterized subroutine WITHOUT 'inline' is auto-inlined (1 parameter)") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
foo(1)
|
|
}
|
|
sub foo(ubyte x) {
|
|
cx16.r0 = x
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Should no longer contain a function call to foo
|
|
val hasFooCall = startSub.statements.any { stmt ->
|
|
stmt is FunctionCallStatement &&
|
|
stmt.target.nameInSource.last() == "foo"
|
|
}
|
|
hasFooCall shouldBe false
|
|
}
|
|
|
|
test("parameterized subroutine with 1 simple argument is inlined (auto-inlining)") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
ubyte @shared a = 10
|
|
ubyte @shared b = add_one(a)
|
|
ubyte @shared c = add_one(20)
|
|
}
|
|
sub add_one(ubyte x) -> ubyte {
|
|
return x + 1
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Should no longer contain a function call to add_one
|
|
val hasAddOneCall = startSub.statements.any { stmt ->
|
|
stmt is FunctionCallStatement &&
|
|
stmt.target.nameInSource.last() == "add_one"
|
|
}
|
|
val hasAddOneCallExpr = startSub.statements.any { stmt ->
|
|
(stmt as? Assignment)?.value.let { it is FunctionCallExpression && it.target.nameInSource.last() == "add_one" }
|
|
}
|
|
hasAddOneCall shouldBe false
|
|
hasAddOneCallExpr shouldBe false
|
|
|
|
// Should have assignments with incremented values or binary expressions
|
|
val assigns = startSub.statements.filterIsInstance<Assignment>()
|
|
assigns.any { (it.value as? BinaryExpression)?.right is NumericLiteral && (it.value as BinaryExpression).operator == "+" } shouldBe true
|
|
}
|
|
|
|
test("parameterized subroutine with 1 simple argument is inlined (manual 'inline')") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
ubyte @shared a = 10
|
|
ubyte @shared b = square(a)
|
|
}
|
|
inline sub square(ubyte x) -> ubyte {
|
|
return x * x
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Should no longer contain a function call to square
|
|
val hasSquareCall = startSub.statements.any { stmt ->
|
|
(stmt as? Assignment)?.value.let { it is FunctionCallExpression && it.target.nameInSource.last() == "square" }
|
|
}
|
|
hasSquareCall shouldBe false
|
|
}
|
|
|
|
test("parameterized subroutine with complex argument is NOT inlined") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
ubyte @shared b = add_one(get_val())
|
|
}
|
|
sub get_val() -> ubyte {
|
|
cx16.r0++
|
|
return 10
|
|
}
|
|
inline sub add_one(ubyte x) -> ubyte {
|
|
return x + 1
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// Should STILL contain a function call to add_one because get_val() has side effects
|
|
val hasAddOneCall = startSub.statements.any { stmt ->
|
|
(stmt as? Assignment)?.value.let { it is FunctionCallExpression && it.target.nameInSource.last() == "add_one" }
|
|
}
|
|
hasAddOneCall shouldBe true
|
|
}
|
|
|
|
|
|
test("memory-mapped IO reads should not be eliminated (c64)") {
|
|
val src = $$"""
|
|
main {
|
|
&ubyte io_reg = $d021
|
|
ubyte @shared result
|
|
sub start() {
|
|
result = io_reg
|
|
result = io_reg
|
|
result = io_reg
|
|
io_reg = 0
|
|
io_reg = 0
|
|
io_reg = 0
|
|
}
|
|
}"""
|
|
val result = compileText(C64Target(), true, src, outputDir, writeAssembly = true)!!
|
|
|
|
// Check the SimpleAst (codegenAst) before assembly: should have 3 reads and 3 writes to io_reg
|
|
val sub = result.codegenAst!!.entrypoint()!!
|
|
val assignments = sub.children.drop(1).filterIsInstance<PtAssignment>()
|
|
val ioRegReads = assignments.count { a ->
|
|
(a.value as? PtIdentifier)?.name?.endsWith("io_reg")==true
|
|
}
|
|
val ioRegWrites = assignments.count { a ->
|
|
a.target.identifier?.name?.endsWith("io_reg")==true
|
|
}
|
|
ioRegReads shouldBe 3
|
|
ioRegWrites shouldBe 3
|
|
|
|
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
|
|
val asm = asmFile.readText()
|
|
|
|
// $d021 is an IO address so every read and write should be there and not optimized away, there should be 3 loads and 3 stores
|
|
val ioAccessCount = asm.lines().count { (it.contains("lda ") || it.contains("sta ") || it.contains("stz ")) && it.contains("io_reg") }
|
|
ioAccessCount shouldBe 6
|
|
}
|
|
|
|
test("memory-mapped IO reads should not be eliminated (cx16)") {
|
|
val src = $$"""
|
|
main {
|
|
&ubyte io_reg = $9f01
|
|
ubyte @shared result
|
|
sub start() {
|
|
result = io_reg
|
|
result = io_reg
|
|
result = io_reg
|
|
io_reg = 0
|
|
io_reg = 0
|
|
io_reg = 0
|
|
}
|
|
}"""
|
|
val result = compileText(Cx16Target(), true, src, outputDir, writeAssembly = true)!!
|
|
|
|
// Check the SimpleAst (codegenAst) before assembly: should have 3 reads and 3 writes to io_reg
|
|
val sub = result.codegenAst!!.entrypoint()!!
|
|
val assignments = sub.children.drop(1).filterIsInstance<PtAssignment>()
|
|
val ioRegReads = assignments.count { a ->
|
|
(a.value as? PtIdentifier)?.name?.endsWith("io_reg")==true
|
|
}
|
|
val ioRegWrites = assignments.count { a ->
|
|
a.target.identifier?.name?.endsWith("io_reg")==true
|
|
}
|
|
ioRegReads shouldBe 3
|
|
ioRegWrites shouldBe 3
|
|
|
|
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
|
|
val asm = asmFile.readText()
|
|
|
|
// $d021 is an IO address so every read and write should be there and not optimized away, there should be 3 loads and 3 stores
|
|
val ioAccessCount = asm.lines().count { (it.contains("lda ") || it.contains("sta ") || it.contains("stz ")) && it.contains("io_reg") }
|
|
ioAccessCount shouldBe 6
|
|
}
|
|
|
|
test("redundant initialization warning only shown for explicit initializers") {
|
|
val srcWithInit = """
|
|
%import textio
|
|
main {
|
|
sub start() {
|
|
ubyte a = 0
|
|
txt.print("gap\n")
|
|
a = 10
|
|
txt.print_ub(a)
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val errorsWithInit = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(VMTarget(), true, srcWithInit, outputDir, errors = errorsWithInit)
|
|
errorsWithInit.warnings.any { "variable 'a' (declared at line 4) is only assigned here" in it } shouldBe true
|
|
|
|
val srcWithoutInit = """
|
|
%import textio
|
|
main {
|
|
sub start() {
|
|
ubyte a
|
|
txt.print("gap\n")
|
|
a = 10
|
|
txt.print_ub(a)
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val errorsWithoutInit = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(VMTarget(), true, srcWithoutInit, outputDir, errors = errorsWithoutInit)
|
|
errorsWithoutInit.warnings.shouldBeEmpty()
|
|
}
|
|
|
|
test("redundant initialization is removed even if warning is suppressed") {
|
|
val srcWithoutInit = """
|
|
main {
|
|
sub start() {
|
|
ubyte a
|
|
cx16.r0++ ; gap
|
|
a = 10
|
|
cx16.r1 = a
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(VMTarget(), true, srcWithoutInit, outputDir)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
// 'a' should now be a CONST so there are no assignments to it anymore
|
|
val assignments = startSub.statements.filterIsInstance<Assignment>()
|
|
assignments.filter { it.target.identifier?.nameInSource?.singleOrNull() == "a" }.size shouldBe 0
|
|
val aDecl = startSub.allDefinedSymbols.find { it.first == "a" }!!.second as VarDecl
|
|
aDecl.type shouldBe VarDeclType.CONST
|
|
aDecl.value!!.constValue(result.compilerAst)!!.number shouldBe 10.0
|
|
}
|
|
|
|
test("inline keyword on regular subroutine") {
|
|
val src = """
|
|
%import textio
|
|
main {
|
|
sub start() {
|
|
txt.print_ub(foo())
|
|
}
|
|
inline sub foo() -> ubyte {
|
|
return 42
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(VMTarget(), true, src, outputDir)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// foo() should be inlined, so there should be a call to txt.print_ub with 42
|
|
val printCall = startSub.statements.filterIsInstance<FunctionCallStatement>()
|
|
.find { it.target.nameInSource.last() == "print_ub" }
|
|
printCall shouldNotBe null
|
|
printCall!!.args[0].constValue(result.compilerAst)!!.number shouldBe 42.0
|
|
|
|
// The original foo() call should be gone
|
|
val fooCall = startSub.statements.any { it is FunctionCallStatement && it.target.nameInSource.last() == "foo" }
|
|
fooCall shouldBe false
|
|
}
|
|
test("multiplication by negative power of two (simpleAst)") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
word @shared x = 10
|
|
word y1 = x * -2
|
|
word y2 = x * -4
|
|
word y3 = -8 * x
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(VMTarget(), true, src, outputDir)!!
|
|
val sub = result.codegenAst!!.entrypoint()!!
|
|
val assignments = sub.children.filterIsInstance<PtAssignment>()
|
|
|
|
val y1 = assignments.find { it.target.identifier?.name?.endsWith("y1") == true }!!.value as PtBinaryExpression
|
|
val y2 = assignments.find { it.target.identifier?.name?.endsWith("y2") == true }!!.value as PtBinaryExpression
|
|
val y3 = assignments.find { it.target.identifier?.name?.endsWith("y3") == true }!!.value as PtBinaryExpression
|
|
|
|
y1.operator shouldBe "<<"
|
|
(y1.left as PtPrefix).operator shouldBe "-"
|
|
(y1.right as PtNumber).number shouldBe 1.0
|
|
|
|
y2.operator shouldBe "<<"
|
|
(y2.left as PtPrefix).operator shouldBe "-"
|
|
(y2.right as PtNumber).number shouldBe 2.0
|
|
|
|
y3.operator shouldBe "<<"
|
|
(y3.left as PtPrefix).operator shouldBe "-"
|
|
(y3.right as PtNumber).number shouldBe 3.0
|
|
}
|
|
|
|
test("self-referential subroutine is not inlined (would cause infinite loop)") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void self_ref()
|
|
cx16.r0++
|
|
}
|
|
sub self_ref() {
|
|
self_ref()
|
|
}
|
|
}
|
|
"""
|
|
val result = compileText(VMTarget(), true, src, outputDir)!!
|
|
val startSub = result.compilerAst.entrypoint
|
|
|
|
// The self-referential call should NOT be inlined - verify the function call still exists
|
|
val hasSelfRefCall = startSub.statements.any { stmt ->
|
|
stmt is FunctionCallStatement && stmt.target.nameInSource.lastOrNull() == "self_ref"
|
|
}
|
|
hasSelfRefCall shouldBe true
|
|
}
|
|
|
|
test("string literals in struct instances are not deduplicated") {
|
|
val src = """
|
|
main {
|
|
struct Node {
|
|
str name
|
|
}
|
|
|
|
^^Node n1 = ^^Node:["test"]
|
|
^^Node n2 = ^^Node:["test"]
|
|
|
|
sub start() {
|
|
cx16.r0 = n1^^.name
|
|
cx16.r1 = n2^^.name
|
|
}
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests()
|
|
val result = compileText(VMTarget(), true, src, outputDir, errors = errors)
|
|
if (result == null) {
|
|
println(errors.errors)
|
|
fail("Compilation failed: " + errors.errors)
|
|
}
|
|
|
|
class InitializerCollector : IAstVisitor {
|
|
val initializers = mutableListOf<StaticStructInitializer>()
|
|
override fun visit(initializer: StaticStructInitializer) {
|
|
initializers.add(initializer)
|
|
// Default traversal
|
|
initializer.structname.accept(this)
|
|
initializer.args.forEach { it.accept(this) }
|
|
}
|
|
}
|
|
|
|
val collector = InitializerCollector()
|
|
result.compilerAst.modules.forEach { it.accept(collector) }
|
|
|
|
collector.initializers.size shouldBe 2
|
|
|
|
// The first argument is the string (the "name" field)
|
|
fun getIdentifier(expr: Expression): IdentifierReference {
|
|
return when (expr) {
|
|
is IdentifierReference -> expr
|
|
is AddressOf -> expr.identifier!!
|
|
else -> fail("Expected IdentifierReference or AddressOf, got " + expr::class.simpleName)
|
|
}
|
|
}
|
|
val name1 = getIdentifier(collector.initializers[0].args[0])
|
|
val name2 = getIdentifier(collector.initializers[1].args[0])
|
|
|
|
// They should point to different interned strings
|
|
name1.nameInSource shouldNotBe name2.nameInSource
|
|
}
|
|
|
|
test("signed modulo strength reduction") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
byte @shared x = -5
|
|
byte @shared res = x % 8
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(VMTarget(), true, src, outputDir)!!
|
|
val sub = result.codegenAst!!.entrypoint()!!
|
|
val assignments = sub.children.filterIsInstance<IPtAssignment>()
|
|
val resAssign = assignments.find { it.target.identifier?.name?.contains("res") == true }
|
|
?: fail("Could not find assignment to 'res'")
|
|
val resMod = resAssign.value
|
|
resMod shouldBe instanceOf<PtBinaryExpression>()
|
|
(resMod as PtBinaryExpression).operator shouldBe "%"
|
|
}
|
|
|
|
test("unsigned comparison wraparound") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
ubyte @shared x = 10
|
|
ubyte @shared y = 0
|
|
bool @shared b1 = x <= (y - 1)
|
|
ubyte @shared y2 = 255
|
|
bool @shared b2 = x >= (y2 + 1)
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(VMTarget(), true, src, outputDir)!!
|
|
val sub = result.codegenAst!!.entrypoint()!!
|
|
val assignments = sub.children.filterIsInstance<IPtAssignment>()
|
|
|
|
val b1Assign = assignments.find { it.target.identifier?.name?.contains("b1") == true }
|
|
?: fail("Could not find assignment to 'b1'")
|
|
val b1 = b1Assign.value as PtBinaryExpression
|
|
// Might be swapped to (y-1) >= x
|
|
if (b1.operator == "<=") {
|
|
(b1.left as PtIdentifier).name shouldContain "x"
|
|
} else {
|
|
b1.operator shouldBe ">="
|
|
(b1.right as PtIdentifier).name shouldContain "x"
|
|
}
|
|
|
|
val b2Assign = assignments.find { it.target.identifier?.name?.contains("b2") == true }
|
|
?: fail("Could not find assignment to 'b2'")
|
|
val b2 = b2Assign.value as PtBinaryExpression
|
|
// Might be swapped to (y2+1) <= x
|
|
if (b2.operator == ">=") {
|
|
(b2.left as PtIdentifier).name shouldContain "x"
|
|
} else {
|
|
b2.operator shouldBe "<="
|
|
(b2.right as PtIdentifier).name shouldContain "x"
|
|
}
|
|
}
|
|
|
|
test("identity folding with side effects (volatility)") {
|
|
val src = $$"""
|
|
main {
|
|
sub start() {
|
|
bool @shared b = peek($d012) == peek($d012)
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val errors = ErrorReporterForTests()
|
|
val result = compileText(C64Target(), true, src, outputDir, errors = errors)
|
|
if (result == null) {
|
|
println(errors.errors)
|
|
fail("Compilation failed: " + errors.errors)
|
|
}
|
|
val sub = result.codegenAst!!.entrypoint()!!
|
|
val assignments = sub.children.filterIsInstance<IPtAssignment>()
|
|
val bAssign = assignments.find { it.target.identifier?.name?.contains("b") == true }
|
|
?: fail("Could not find assignment to 'b'")
|
|
val b = bAssign.value
|
|
|
|
// Should not be folded to 'true'
|
|
b shouldBe instanceOf<PtBinaryExpression>()
|
|
(b as PtBinaryExpression).operator shouldBe "=="
|
|
b.left shouldBe instanceOf<PtMemoryByte>()
|
|
b.right shouldBe instanceOf<PtMemoryByte>()
|
|
}
|
|
|
|
test("floating-point algebraic identities") {
|
|
val src = """
|
|
%option enable_floats
|
|
main {
|
|
sub start() {
|
|
float @shared f = 0.0
|
|
float @shared res1 = f * 0.0
|
|
float @shared res2 = 0.0 * f
|
|
float @shared res3 = 0.0 / f
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val errors = ErrorReporterForTests()
|
|
val result = compileText(VMTarget(), true, src, outputDir, errors = errors)
|
|
if (result == null) {
|
|
println(errors.errors)
|
|
fail("Compilation failed: " + errors.errors)
|
|
}
|
|
val sub = result.codegenAst!!.entrypoint()!!
|
|
val assignments = sub.children.filterIsInstance<IPtAssignment>()
|
|
|
|
val res1Assign = assignments.find { it.target.identifier?.name?.contains("res1") == true }!!
|
|
res1Assign.value shouldBe instanceOf<PtBinaryExpression>()
|
|
(res1Assign.value as PtBinaryExpression).operator shouldBe "*"
|
|
|
|
val res2Assign = assignments.find { it.target.identifier?.name?.contains("res2") == true }!!
|
|
res2Assign.value shouldBe instanceOf<PtBinaryExpression>()
|
|
(res2Assign.value as PtBinaryExpression).operator shouldBe "*"
|
|
|
|
val res3Assign = assignments.find { it.target.identifier?.name?.contains("res3") == true }!!
|
|
res3Assign.value shouldBe instanceOf<PtBinaryExpression>()
|
|
(res3Assign.value as PtBinaryExpression).operator shouldBe "/"
|
|
}
|
|
test("asmsub arguments correctly handled when inlined (assembly check)") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
ubyte @shared a = 10
|
|
call_asm(a)
|
|
}
|
|
inline sub call_asm(ubyte x) {
|
|
my_asmsub(x + 1)
|
|
}
|
|
asmsub my_asmsub(ubyte val @A) {
|
|
%asm {{
|
|
sta $02
|
|
rts
|
|
}}
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val result = compileText(Cx16Target(), true, src, outputDir, writeAssembly = true)!!
|
|
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
|
|
val asm = asmFile.readText()
|
|
// verify that the argument passing code (inc a for x+1) is present
|
|
asm shouldContain "inc a"
|
|
asm shouldContain "p8s_my_asmsub"
|
|
}
|
|
|
|
test("inliner relinks parent pointers (static check)") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
call_asm(10)
|
|
}
|
|
inline sub call_asm(ubyte x) {
|
|
foo(x)
|
|
}
|
|
sub foo(ubyte x) {}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(Cx16Target(), true, src, outputDir)!!
|
|
val program = result.compilerAst
|
|
val root = program.namespace
|
|
|
|
fun checkParents(node: Node, expectedParent: Node) {
|
|
if (node !== root as Node && node.parent !== expectedParent && expectedParent !== ParentSentinel) {
|
|
fail("Parent mismatch at ${node::class.simpleName} ($node): expected $expectedParent but got ${node.parent}")
|
|
}
|
|
when (node) {
|
|
is IStatementContainer -> node.statements.forEach { checkParents(it, node) }
|
|
is Subroutine -> {
|
|
node.parameters.forEach { checkParents(it, node) }
|
|
node.statements.forEach { checkParents(it, node) }
|
|
}
|
|
is FunctionCallStatement -> {
|
|
checkParents(node.target, node)
|
|
node.args.forEach { checkParents(it, node) }
|
|
}
|
|
is FunctionCallExpression -> {
|
|
checkParents(node.target, node)
|
|
node.args.forEach { checkParents(it, node) }
|
|
}
|
|
is BinaryExpression -> {
|
|
checkParents(node.left, node)
|
|
checkParents(node.right, node)
|
|
}
|
|
is PrefixExpression -> checkParents(node.expression, node)
|
|
is Return -> node.values.forEach { checkParents(it, node) }
|
|
is Assignment -> {
|
|
checkParents(node.target, node)
|
|
checkParents(node.value, node)
|
|
}
|
|
is VarDecl -> node.value?.let { checkParents(it, node) }
|
|
is Module -> node.statements.forEach { checkParents(it, node) }
|
|
is GlobalNamespace -> node.modules.forEach { checkParents(it, node) }
|
|
is Block -> node.statements.forEach { checkParents(it, node) }
|
|
}
|
|
}
|
|
|
|
checkParents(root, ParentSentinel)
|
|
}
|
|
|
|
test("A = A +/- (B +/- N) split keeps correct values (sign-bug regression)") {
|
|
// Regression test for the "A = A +/- (B +/- N)" split in StatementOptimizer.
|
|
// The split rewrites A = A op1 (B op2 N) into A = A op1 B ; A = A secondOp N
|
|
// where secondOp must keep the combined expression equivalent:
|
|
// A+(B+N)=A+N, A+(B-N)=A-N, A-(B+N)=A-N, A-(B-N)=A+N
|
|
// The buggy version hardcoded secondOp = op2, which gave the wrong sign
|
|
// for the A-... cases (e.g. A-(B-N) became A-B-N instead of A-B+N).
|
|
val source = """
|
|
main {
|
|
sub start() {
|
|
uword @shared a = 10
|
|
uword @shared b = 3
|
|
a = a + (b + 5) ; 10+3+5 = 18
|
|
|
|
uword @shared c = 20
|
|
uword @shared d = 4
|
|
c = c - (d - 2) ; 20-(4-2) = 18
|
|
|
|
uword @shared g = 50
|
|
uword @shared h = 5
|
|
g = g - (h + 3) ; 50-(5+3) = 42
|
|
|
|
uword @shared e = 100
|
|
uword @shared f = 7
|
|
e = e + (f - 3) ; 100+7-3 = 104
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(VMTarget(), true, source, outputDir)!!
|
|
val irFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
|
|
val irContent = irFile.readText()
|
|
val irProgram = IRFileReader().read(irContent)
|
|
irProgram.st.stripAllPrefixes()
|
|
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
|
|
|
|
VmRunner().runAndTestProgram(irContent) { vm ->
|
|
// a = a + (b + 5) -> 10+3+5 = 18
|
|
vm.memory.getUW(allocations["main.start.a"]!!) shouldBe 18u
|
|
// c = c - (d - 2) -> 20-(4-2) = 18 (buggy split gave 14)
|
|
vm.memory.getUW(allocations["main.start.c"]!!) shouldBe 18u
|
|
// g = g - (h + 3) -> 50-(5+3) = 42 (buggy split gave 48)
|
|
vm.memory.getUW(allocations["main.start.g"]!!) shouldBe 42u
|
|
// e = e + (f - 3) -> 100+7-3 = 104
|
|
vm.memory.getUW(allocations["main.start.e"]!!) shouldBe 104u
|
|
}
|
|
}
|
|
|
|
test("A = A +/- (B * N) is NOT split (only additive inner op splits)") {
|
|
// Regression: the split optimization may only fire when the inner operator
|
|
// is + or - (e.g. A = A - (B * N) must stay a subtraction of a product).
|
|
// The midpoint disc algorithm in monogfx relies on this:
|
|
// decisionOver2 -= radius * 2 must NOT become decisionOver2 = decisionOver2 - radius ; decisionOver2 = decisionOver2 + 2
|
|
val source = """
|
|
main {
|
|
sub start() {
|
|
word @shared d = 5
|
|
ubyte @shared r = 3
|
|
r++
|
|
d -= r * $0002 ; 5 - (4*2) = -3
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val result = compileText(VMTarget(), true, source, outputDir)!!
|
|
val irFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
|
|
val irContent = irFile.readText()
|
|
val irProgram = IRFileReader().read(irContent)
|
|
irProgram.st.stripAllPrefixes()
|
|
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
|
|
|
|
VmRunner().runAndTestProgram(irContent) { vm ->
|
|
// if the multiplier had been wrongly split into "- r ; + 2" the result would be 5-4+2 = 3
|
|
vm.memory.getSW(allocations["main.start.d"]!!) shouldBe (-3).toShort()
|
|
}
|
|
}
|
|
})
|