Files
prog8/compiler/test/vm/TestCompilerVirtual.kt
T

1956 lines
66 KiB
Kotlin

package prog8tests.vm
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.collections.shouldBeIn
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.ints.shouldBeLessThanOrEqual
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
import prog8.ast.expressions.MemorySlabRef
import prog8.ast.statements.Assignment
import prog8.code.SymbolNames
import prog8.code.target.C64Target
import prog8.code.target.Cx16Target
import prog8.code.target.VMTarget
import prog8.intermediate.*
import prog8.vm.VmRunner
import prog8.vm.VmVariableAllocator
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
import prog8tests.helpers.shouldContainInOrder
import kotlin.io.path.readText
class TestCompilerVirtual: FunSpec({
val outputDir = tempdir().toPath()
test("linear words array with pointers") {
val src = """
main {
sub start() {
str localstr = "hello"
ubyte[] otherarray = [1,2,3]
pointer[] @nosplit words = [1111,2222,"three",&localstr,&otherarray]
pointer @shared zz = &words
zz = words[2]
zz++
zz = words[3]
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText(), false)
}
test("split words array with pointers") {
val src = """
main {
sub start() {
str localstr = "hello"
ubyte[] otherarray = [1,2,3]
pointer[] words = [1111,2222,"three",&localstr,&otherarray]
pointer @shared zz = &words
zz = words[2]
zz++
zz = words[3]
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText(), false)
}
test("taking address of split arrays works") {
val src="""
main {
sub start() {
cx16.r0L=0
if cx16.r0L==0 {
pointer[] addresses = [scores2, start]
uword[] scores1 = [10, 25, 50, 100]
uword[] scores2 = [100, 250, 500, 1000]
pointer addr1 = &scores1
pointer addr2 = &scores2
pointer addr3 = &addresses
}
}
}"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly=true, errors=errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.warnings.size shouldBe 0
}
test("compile virtual: str args and return type, and global var init") {
val src = """
main {
ubyte @shared dvar = test.dummy()
sub start() {
sub testsub(str s1) -> str {
return "result"
}
uword result = testsub("arg")
}
}
test {
sub dummy() -> ubyte {
cx16.r0++
return 80
}
}"""
val target = VMTarget()
var result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
var virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText(), false)
result = compileText(target, true, src, outputDir, writeAssembly = true)!!
virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText(), false)
}
test("compile virtual: nested labels") {
val src = """
main {
sub start() {
uword i
uword k
repeat {
mylabel0:
goto mylabel0
}
while cx16.r0==0 {
mylabel1:
goto mylabel1
}
do {
mylabel2:
goto mylabel2
} until cx16.r0==1
repeat cx16.r0 {
mylabel3:
goto mylabel3
}
for cx16.r0L in 0 to 2 {
mylabel4:
goto mylabel4
}
for cx16.r0L in cx16.r1L to cx16.r2L {
mylabel5:
goto mylabel5
}
mylabel_outside:
for i in 0 to 10 {
mylabel_inside:
if i==100 {
goto mylabel_outside
goto mylabel_inside
}
while k <= 10 {
k++
}
do {
k--
} until k==0
for k in 0 to 5 {
i++
}
repeat 10 {
k++
}
}
}
}"""
val target1 = C64Target()
compileText(target1, false, src, outputDir, writeAssembly = false) shouldNotBe null
val target = VMTarget()
compileText(target, false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("augmented assignment with constant 2 on pointer field") {
// regression: the INC/DEC optimization condition was not properly parenthesized,
// so any operator combined with a const value of 2 took the INC/DEC path (e.g. p.val >>= 2 became p.val -= 2)
val src = """
main {
struct Node {
uword val
^^Node next
}
sub start() {
^^Node p = ^^Node: [0, 0]
p.val = 500
p.val >>= 2
main.shift_result = p.val
p.val <<= 2
main.shiftl_result = p.val
p.val += 2
main.add_result = p.val
p.val -= 2
main.sub_result = p.val
}
uword @shared shift_result
uword @shared shiftl_result
uword @shared add_result
uword @shared sub_result
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
vm.memory.getUW(allocations["main.shift_result"]!!) shouldBe 125u
vm.memory.getUW(allocations["main.shiftl_result"]!!) shouldBe 500u
vm.memory.getUW(allocations["main.add_result"]!!) shouldBe 502u
vm.memory.getUW(allocations["main.sub_result"]!!) shouldBe 500u
}
}
test("case sensitive symbols") {
val src = """
main {
sub start() {
ubyte @shared bytevar = 11 ; var at 0
ubyte @shared byteVAR = 22 ; var at 1
ubyte @shared ByteVar = 33 ; var at 2
ubyte @shared total = bytevar+byteVAR+ByteVar ; var at 3
goto skipLABEL
SkipLabel:
return
skipLABEL:
bytevar = 42
}
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText()) { vm ->
vm.memory.getUB(0u) shouldBe 42u
vm.memory.getUB(3u) shouldBe 66u
}
}
test("memory slabs") {
val src = """
main {
sub start() {
pointer slab1 = memory("slab1", 2000, 64)
slab1[10]=42
slab1[11]=43
ubyte @shared value1 = slab1[10] ; var at 2
ubyte @shared value2 = slab1[11] ; var at 3
}
}"""
val target = VMTarget()
val result = compileText(target, false, src, outputDir, writeAssembly = true)!!
val start = result.compilerAst.entrypoint
start.statements.size shouldBe 10
(start.statements[2] as Assignment).value as MemorySlabRef
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irSrc = virtfile.readText()
val irProgram = IRFileReader().read(irSrc)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irSrc) { vm ->
val slabAddr = allocations["prog8_slabs.memory_slab1"]!!
vm.memory.getUB(slabAddr + 10u) shouldBe 42u
vm.memory.getUB(slabAddr + 11u) shouldBe 43u
}
}
test("memory mapped var as for loop counter") {
val src = """
main {
sub start() {
for cx16.r0 in 0 to 10 {
cx16.r1++
}
}
}"""
val othertarget = Cx16Target()
compileText(othertarget, true, src, outputDir, writeAssembly = true) shouldNotBe null
val target = VMTarget()
var result = compileText(target, true, src, outputDir, writeAssembly = true)!!
var virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText()) { vm ->
vm.stepCount shouldBe 47
}
result = compileText(target, false, src, outputDir, writeAssembly = true)!!
virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText()) { vm ->
vm.stepCount shouldBe 47
}
}
test("implicit for loop var execute over ranges and arrays") {
val src = """
main {
uword @shared range_sum
uword @shared array_sum
sub start() {
for i in 1 to 5 {
range_sum += i
}
ubyte[] values = [2, 4, 6]
for value in values {
array_sum += value
}
}
}
"""
val result = compileText(VMTarget(), optimize = true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irSrc = virtfile.readText()
val irProgram = IRFileReader().read(irSrc)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irSrc) { vm ->
vm.memory.getUW(allocations["main.range_sum"]!!) shouldBe 15u
vm.memory.getUW(allocations["main.array_sum"]!!) shouldBe 12u
}
}
test("inline asm for virtual target should be IR") {
val src = """
main {
sub start() {
%asm {{
lda #99
tay
rts
}}
}
}"""
val othertarget = Cx16Target()
compileText(othertarget, true, src, outputDir, writeAssembly = true) shouldNotBe null
val target = VMTarget()
val result = compileText(target, false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val exc = shouldThrow<Exception> {
VmRunner().runProgram(virtfile.readText(), false)
}
exc.message shouldContain("cannot execute native assembly code")
}
test("inline asm for virtual target with IR is accepted and converted to regular instructions") {
val src = """
main {
sub start() {
%ir {{
incm.b [$2000]
return
}}
}
}"""
val target = VMTarget()
val result = compileText(target, false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irSrc = virtfile.readText()
irSrc.shouldContainInOrder("incm.b [$2000]")
irSrc.shouldNotContain("</ASM>")
VmRunner().runProgram(irSrc, false)
}
test("addresses from labels/subroutines are loaded as artificial label addresses") {
val src = """
main {
sub start() {
mylabel:
ubyte variable
pointer @shared pointer1 = &main.start
pointer @shared pointer2 = &start
pointer @shared pointer3 = &main.start.mylabel
pointer @shared pointer4 = &mylabel
pointer[] @shared ptrs = [&variable, &start, &main.start, &mylabel, &main.start.mylabel]
}
}
"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irSrc = virtfile.readText()
val irProgram = IRFileReader().read(irSrc)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irSrc) { vm ->
val startAddr = vm.artificialLabelAddresses.entries.single { SymbolNames.stripPrefixes(it.value.label ?: "") == "main.start" }.key
val mylabelAddr = vm.artificialLabelAddresses.entries.single { SymbolNames.stripPrefixes(it.value.label ?: "") == "main.start.mylabel" }.key
startAddr shouldNotBe 0u
mylabelAddr shouldNotBe 0u
mylabelAddr shouldNotBe startAddr
val varAddr = allocations["main.start.variable"]!!
vm.memory.getUL(allocations["main.start.pointer1"]!!) shouldBe startAddr
vm.memory.getUL(allocations["main.start.pointer2"]!!) shouldBe startAddr
vm.memory.getUL(allocations["main.start.pointer3"]!!) shouldBe mylabelAddr
vm.memory.getUL(allocations["main.start.pointer4"]!!) shouldBe mylabelAddr
val ptrsAddr = allocations["main.start.ptrs"]!!
vm.memory.getUL(ptrsAddr) shouldBe varAddr
vm.memory.getUL(ptrsAddr + 4u) shouldBe startAddr
vm.memory.getUL(ptrsAddr + 8u) shouldBe startAddr
vm.memory.getUL(ptrsAddr + 12u) shouldBe mylabelAddr
vm.memory.getUL(ptrsAddr + 16u) shouldBe mylabelAddr
}
}
test("address of a block produces LOAD with block label in IR") {
val src = $$"""
main $1000 {
sub start() {
other.start()
}
}
other {
sub start() {
pointer @shared ptr = &main
}
}
"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irSrc = virtfile.readText()
// the &main must be encoded as a LOAD with the block name as label symbol
irSrc.shouldContain("load.p r1.p,#main")
// and the block must have an address that a 6502 backend can resolve
irSrc.shouldContain($$"""<BLOCK NAME="main" ADDRESS="$1000"""")
// the other block's subroutine must contain the LOAD
val irProgram = IRFileReader().read(virtfile)
val otherSub = irProgram.blocks.single { it.label == "other" }.children.single() as IRSubroutine
val loadInstr = otherSub.chunks.flatMap { it.instructions }.single { it.opcode == Opcode.LOAD }
(loadInstr.immediate as ImmediateOperand.SymbolAddress).symbol shouldBe "main"
}
test("nesting with overlapping names is ok (doesn't work for 64tass)") {
val src="""
main {
sub start() {
main()
main.start.start()
main.main()
sub main() {
cx16.r0++
}
sub start() {
cx16.r0++
}
}
sub main() {
cx16.r0++
}
}"""
val target = VMTarget()
compileText(target, false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("compile virtual: short code for if-goto") {
val src = """
main {
sub start() {
if_cc
goto ending
if_cs
goto ending
if cx16.r0==0 goto ending
if cx16.r0!=0 goto ending
if cx16.r0s>0 goto ending
if cx16.r0s<0 goto ending
ending:
}
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
result.compilerAst.entrypoint.statements.size shouldBe 8
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val start = irProgram.blocks[0].children[0] as IRSubroutine
val instructions = start.chunks.flatMap { c->c.instructions }
// Was 11 before the strict status-bits contract change; now 13 because the
// VIRTUAL target has statusBitsOnMultiByteOps=false, so explicit CMPI #0
// instructions are emitted before the "compare with zero" branches instead
// of relying on the previous instruction's Z flag. The 2 extra CMPIs come
// from the unsigned compare-with-zero branches (== 0 and != 0). The signed
// compares (> 0 and < 0) already use a CMP instruction that sets Z.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
instructions.size shouldBe 13
instructions.last().opcode shouldBe Opcode.RETURN
}
test("if-expression always emits explicit CMPI when condition sets flags (strict status-bits contract)") {
val src = """
main {
sub start() {
bool @shared flag = true
cx16.r0L = if flag then 1 else 0
}
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly=true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val start = irProgram.blocks[0].children[0] as IRSubroutine
val instructions = start.chunks.flatMap { c->c.instructions }
// Find LOADM for 'flag' variable - use contains match since scoped name may vary
val loadmIdx = instructions.indexOfFirst { it.opcode == Opcode.LOADM && it.memory?.symbolName?.contains("flag") == true }
loadmIdx shouldBeGreaterThan -1
// The VIRTUAL target has statusBitsOnMultiByteOps=false, so the IR generator
// always emits an explicit CMPI #0 before the BSTEQ/BSTNE branch, even though
// the previous LOADM is documented to set Z. The strict contract requires an
// explicit compare so the result is correct for any register width (BYTE/WORD/LONG).
val nextInstr = instructions[loadmIdx + 1]
nextInstr.opcode shouldBe Opcode.CMPI
nextInstr.immediate?.integerValue shouldBe 0
}
test("if-statement always emits explicit CMPI when condition sets flags (strict status-bits contract)") {
val src = """
main {
sub start() {
bool @shared flag = true
if flag {
cx16.r0L = 1
} else {
cx16.r0L = 0
}
}
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly=true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val start = irProgram.blocks[0].children[0] as IRSubroutine
val instructions = start.chunks.flatMap { c->c.instructions }
// Find LOADM for 'flag' variable - use contains match since scoped name may vary
val loadmIdx = instructions.indexOfFirst { it.opcode == Opcode.LOADM && it.memory?.symbolName?.contains("flag") == true }
loadmIdx shouldBeGreaterThan -1
// Strict contract: the CMPI is always present, never optimized away.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val nextInstr = instructions[loadmIdx + 1]
nextInstr.opcode shouldBe Opcode.CMPI
nextInstr.immediate?.integerValue shouldBe 0
}
test("if-expression omits CMPI since CALL now sets flags") {
val src = """
main {
sub getflag() -> bool { return true }
sub start() {
cx16.r0L = if getflag() then 1 else 0
}
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly=true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val start = irProgram.blocks[0].children[0] as IRSubroutine
val instructions = start.chunks.flatMap { c->c.instructions }
// Should NOT contain CMPI #0 after function call anymore (since CALL effectively sets flags now)
val cmpiInstr = instructions.find { it.opcode == Opcode.CMPI && it.immediate?.integerValue == 0 }
cmpiInstr shouldBe null
}
test("repeat counts (const)") {
val src="""
main {
sub start() {
cx16.r0 = 0
repeat 255 {
cx16.r0++
}
repeat 256 {
cx16.r0++
}
repeat 257 {
cx16.r0++
}
repeat 1023 {
cx16.r0++
}
repeat 1024 {
cx16.r0++
}
repeat 1025 {
cx16.r0++
}
repeat 65534 {
cx16.r0++
}
repeat 65535 {
cx16.r0++
}
repeat 0 {
cx16.r0++
}
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val start = result.codegenAst!!.entrypoint()!!
start.children.size shouldBe 12
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText()) { vm ->
vm.memory.getUW(0xff0000u) shouldBe 3837u // $ff0000 = cx16.r0
}
}
test("repeat counts (variable)") {
val src="""
main {
sub start() {
uword count
cx16.r0 = 0
count=255
repeat count {
cx16.r0++
}
count=256
repeat count {
cx16.r0++
}
count=257
repeat count {
cx16.r0++
}
count=1023
repeat count {
cx16.r0++
}
count=1024
repeat count {
cx16.r0++
}
count=1025
repeat count {
cx16.r0++
}
count=65534
repeat count {
cx16.r0++
}
count=65535
repeat count {
cx16.r0++
}
count=0
repeat count {
cx16.r0++
}
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val start = result.codegenAst!!.entrypoint()!!
start.children.size shouldBe 22
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText()) { vm ->
vm.memory.getUW(0xff0000u) shouldBe 3837u // $ff0000 = cx16.r0
}
}
test("IRLoopChunk repeat 5 executes 5 times and preserves trip") {
val src="""
main {
sub start() {
ubyte @shared counter = 0
repeat 5 {
counter++
}
}
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irText = virtfile.readText()
// IRLoopChunk should be serialized as <LOOP TRIP="5">
irText shouldContain """TRIP="5""""
irText shouldNotContain "p8_regfile"
val irProgram = IRFileReader().read(virtfile)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irText) { vm ->
vm.memory.getUB(allocations["main.start.counter"]!!) shouldBe 5u
}
// also verify nested loops don't clobber
val nestedSrc="""
main {
sub start() {
ubyte @shared outer = 0
ubyte @shared inner = 0
repeat 3 {
outer++
repeat 2 {
inner++
}
}
}
}"""
val nestedResult = compileText(VMTarget(), true, nestedSrc, outputDir, writeAssembly = true)!!
val nestedVirt = nestedResult.compilationOptions.outputDir.resolve(nestedResult.compilerAst.name + ".p8ir").readText()
nestedVirt shouldContain """TRIP="3""""
nestedVirt shouldContain """TRIP="2""""
val nestedIr = IRFileReader().read(nestedVirt)
nestedIr.st.stripAllPrefixes()
val nestedAlloc = VmVariableAllocator(nestedIr.st, nestedIr.encoding, nestedIr.options.compTarget).allocations
VmRunner().runAndTestProgram(nestedVirt) { vm ->
vm.memory.getUB(nestedAlloc["main.start.outer"]!!) shouldBe 3u
vm.memory.getUB(nestedAlloc["main.start.inner"]!!) shouldBe 6u
}
}
test("asm chunk labels in IR code") {
val src="""
main {
sub start() {
instructions.match()
}
}
instructions {
asmsub match() {
%asm {{
rts
}}
}
%asm {{
nop
}}
}"""
compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("IR codegen for while loop with shortcircuit") {
val src="""
main {
sub start() {
cx16.r0L=1
while cx16.r0L < 10 and cx16.r0L>0 {
cx16.r0L++
}
}
}"""
compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("push() and pop() generate correct IR instructions") {
val src="""
main {
sub start() {
ubyte bb
uword ww
push(42)
bb++
bb=pop()
pushw(9999)
ww++
ww=popw()
}
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val start = irProgram.blocks[0].children[0] as IRSubroutine
val instructions = start.chunks.flatMap { c->c.instructions }
instructions.size shouldBe 13
instructions[3].opcode shouldBe Opcode.PUSH
instructions[3].type shouldBe IRDataType.BYTE
instructions[5].opcode shouldBe Opcode.POP
instructions[5].type shouldBe IRDataType.BYTE
instructions[8].opcode shouldBe Opcode.PUSH
instructions[8].type shouldBe IRDataType.WORD
instructions[10].opcode shouldBe Opcode.POP
instructions[10].type shouldBe IRDataType.WORD
}
test("typed address-of a const pointer with non-const array indexing") {
val src= """
main {
sub start() {
const uword cbuffer = $2000
uword @shared buffer = $2000
cx16.r2 = @(cbuffer + cx16.r0)
cx16.r1 = &cbuffer[cx16.r0]
cx16.r3 = @(buffer + cx16.r0)
cx16.r4 = &buffer[cx16.r0]
}
}"""
val result = compileText(VMTarget(), false, src, outputDir)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText(), true)
}
test("correct reg types with sqrt") {
val src= """
main {
sub start() {
cx16.r0L= sqrt(cx16.r1)
}
}"""
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
}
test("SSA basic blocks") {
val src= """
main {
sub start() {
func()
}
sub func() {
if cx16.r0<10 or cx16.r0>319 {
cx16.r1++
}
}
}"""
val result = compileText(VMTarget(), false, src, outputDir)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val func = irProgram.blocks[0].children[1] as IRSubroutine
func.label shouldBe "main.func"
func.chunks.size shouldBe 4
for(chunk in func.chunks) {
if(chunk.next == null) {
chunk.instructions.last().opcode shouldBeIn OpcodesThatBranchUnconditionally
}
val branches = chunk.instructions.filter { it.opcode in OpcodesThatEndSSAblock }
branches.size shouldBeLessThanOrEqual 1
}
}
test("deeply scoped variable references") {
val src= """
main {
sub start() {
main.sub1.sub2.sub3.variable = 100
}
sub sub1() {
sub sub2() {
sub sub3() {
ubyte @shared variable
}
}
}
}"""
compileText(Cx16Target(), true, src, outputDir) shouldNotBe null
val result = compileText(VMTarget(), true, src, outputDir)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText(), true)
}
test("uninitialized struct instance is zeroed out in BSS") {
val src = """
main {
struct Point {
word x
word y
}
sub start() {
; Create an uninitialized struct instance - should be zeroed out at startup
^^Point emptyPoint = ^^Point : []
; Copy struct fields to shared variables so we can verify them
main.xval = emptyPoint.x
main.yval = emptyPoint.y
}
; Shared variables to store results
word @shared xval
word @shared yval
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
// Verify the IR file contains STRUCTINSTANCESNOINIT section
irContent.shouldContain("<STRUCTINSTANCESNOINIT>")
irContent.shouldContain("</STRUCTINSTANCESNOINIT>")
// Parse the IR to get variable allocations and verify memory
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
// Get the addresses of the shared variables
val xvalAddr = allocations["main.xval"]!!
val yvalAddr = allocations["main.yval"]!!
// These should be 0 because they were copied from the zeroed-out struct
vm.memory.getUW(xvalAddr) shouldBe 0u
vm.memory.getUW(yvalAddr) shouldBe 0u
// Also verify the struct instance itself is zeroed
val structName = irProgram.st.allStructInstances().first().name
val structAddr = allocations[structName]!!
vm.memory.getUW(structAddr) shouldBe 0u // x field
vm.memory.getUW(structAddr + 2u) shouldBe 0u // y field
}
}
test("uninitialized struct instance with more fields is zeroed out") {
val src = """
main {
struct Data {
byte a
byte b
word c
ubyte d
uword e
}
sub start() {
; Create an uninitialized struct instance with multiple fields
^^Data empty = ^^Data : []
; Copy all fields to shared variables to verify they are zeroed
main.a_result = empty.a
main.b_result = empty.b
main.c_result = empty.c
main.d_result = empty.d
main.e_result = empty.e
}
; Shared variables at known addresses to store results
byte @shared a_result
byte @shared b_result
word @shared c_result
ubyte @shared d_result
uword @shared e_result
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
// Verify the IR file contains STRUCTINSTANCESNOINIT section
irContent.shouldContain("<STRUCTINSTANCESNOINIT>")
// Parse the IR to get variable allocations and verify memory
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
// Run the program and verify all struct fields were zeroed
VmRunner().runAndTestProgram(irContent) { vm ->
// Check all shared variables are 0
vm.memory.getUB(allocations["main.a_result"]!!) shouldBe 0u
vm.memory.getUB(allocations["main.b_result"]!!) shouldBe 0u
vm.memory.getUW(allocations["main.c_result"]!!) shouldBe 0u
vm.memory.getUB(allocations["main.d_result"]!!) shouldBe 0u
vm.memory.getUW(allocations["main.e_result"]!!) shouldBe 0u
}
}
test("mix of initialized and uninitialized struct instances") {
val src = """
main {
struct Point {
word x
word y
}
sub start() {
; Initialized struct instance
^^Point initPoint = ^^Point : [100, 200]
; Uninitialized struct instance - should be zeroed
^^Point emptyPoint = ^^Point : []
; Another initialized one
^^Point initPoint2 = ^^Point : [300, 400]
; Another uninitialized one
^^Point emptyPoint2 = ^^Point : []
; Copy values to shared variables for verification
main.x1 = initPoint.x
main.y1 = initPoint.y
main.ex1 = emptyPoint.x
main.ey1 = emptyPoint.y
main.x2 = initPoint2.x
main.y2 = initPoint2.y
main.ex2 = emptyPoint2.x
main.ey2 = emptyPoint2.y
}
; Shared variables to store results
word @shared x1
word @shared y1
word @shared ex1
word @shared ey1
word @shared x2
word @shared y2
word @shared ex2
word @shared ey2
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
// Verify the IR file contains both sections
irContent.shouldContainInOrder("<STRUCTINSTANCESNOINIT>")
irContent.shouldContainInOrder("<STRUCTINSTANCES>")
// Parse the IR to get variable allocations
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
// Run the program and verify both initialized and uninitialized values
VmRunner().runAndTestProgram(irContent) { vm ->
// Check initialized struct values
vm.memory.getUW(allocations["main.x1"]!!) shouldBe 100u
vm.memory.getUW(allocations["main.y1"]!!) shouldBe 200u
vm.memory.getUW(allocations["main.x2"]!!) shouldBe 300u
vm.memory.getUW(allocations["main.y2"]!!) shouldBe 400u
// Check uninitialized struct values (should be zeroed)
vm.memory.getUW(allocations["main.ex1"]!!) shouldBe 0u
vm.memory.getUW(allocations["main.ey1"]!!) shouldBe 0u
vm.memory.getUW(allocations["main.ex2"]!!) shouldBe 0u
vm.memory.getUW(allocations["main.ey2"]!!) shouldBe 0u
}
}
test("struct with pointer fields can be compiled and run") {
val src = """
main {
struct State {
uword c
^^State out
^^State out1
uword lastlist
}
sub start() {
^^State matchstate = [258, 0, 0, 0]
main.matchstatePtr = matchstate
main.lastlistPtr = &matchstate.lastlist
}
^^State @shared matchstatePtr
pointer @shared lastlistPtr
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
irContent.shouldContainInOrder("<STRUCTINSTANCESNOINIT>")
irContent.shouldContainInOrder("^^main.State")
VmRunner().runProgram(irContent, false)
}
test("nosplit word array += and -= with variable index and value") {
// This test verifies the fix for LOADX/STOREX with element size multiplication
// Previously failed with -noopt because index wasn't multiplied by element size
val src = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
; Test += with variable index, constant value
uword[] @nosplit arr1 = [100, 200, 300]
ubyte idx1 = 1
arr1[idx1] += 100
; Test += with variable index, variable value
uword[] @nosplit arr2 = [100, 200, 300]
ubyte idx2 = 2
uword val2 = 25
arr2[idx2] += val2
; Test -= with variable index, variable value
uword[] @nosplit arr3 = [500, 400, 300]
ubyte idx3 = 1
uword val3 = 150
arr3[idx3] -= val3
; Test *= with variable index, constant value
uword[] @nosplit arr4 = [10, 20, 30]
ubyte idx4 = 2
arr4[idx4] *= 5
; Store results for verification
main.r1 = arr1[0]
main.r2 = arr1[1]
main.r3 = arr1[2]
main.r4 = arr2[0]
main.r5 = arr2[1]
main.r6 = arr2[2]
main.r7 = arr3[0]
main.r8 = arr3[1]
main.r9 = arr3[2]
main.r10 = arr4[0]
main.r11 = arr4[1]
main.r12 = arr4[2]
}
uword @shared r1
uword @shared r2
uword @shared r3
uword @shared r4
uword @shared r5
uword @shared r6
uword @shared r7
uword @shared r8
uword @shared r9
uword @shared r10
uword @shared r11
uword @shared r12
}"""
// Test with optimizations OFF to ensure the non-optimized path works
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
// Parse and run
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
// arr1: [100, 200+100, 300] = [100, 300, 300]
vm.memory.getUW(allocations["main.r1"]!!) shouldBe 100u
vm.memory.getUW(allocations["main.r2"]!!) shouldBe 300u
vm.memory.getUW(allocations["main.r3"]!!) shouldBe 300u
// arr2: [100, 200, 300+25] = [100, 200, 325]
vm.memory.getUW(allocations["main.r4"]!!) shouldBe 100u
vm.memory.getUW(allocations["main.r5"]!!) shouldBe 200u
vm.memory.getUW(allocations["main.r6"]!!) shouldBe 325u
// arr3: [500, 400-150, 300] = [500, 250, 300]
vm.memory.getUW(allocations["main.r7"]!!) shouldBe 500u
vm.memory.getUW(allocations["main.r8"]!!) shouldBe 250u
vm.memory.getUW(allocations["main.r9"]!!) shouldBe 300u
// arr4: [10, 20, 30*5] = [10, 20, 150]
vm.memory.getUW(allocations["main.r10"]!!) shouldBe 10u
vm.memory.getUW(allocations["main.r11"]!!) shouldBe 20u
vm.memory.getUW(allocations["main.r12"]!!) shouldBe 150u
}
}
test("divmod does not crash when results are unused (optimization issue)") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
ubyte num = 230
ubyte div = 13
ubyte d, r = divmod(num, div)
}
}
""".trimIndent()
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("divmod results are correct with optimizations") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
ubyte d_res
ubyte r_res
sub start() {
d_res = 0
r_res = 0
ubyte num = 230
ubyte div = 13
ubyte d, r = divmod(num, div)
d_res = d
r_res = r
}
}
""".trimIndent()
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irSource = virtfile.readText()
val irProgram = IRFileReader().read(irSource)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irSource) { vm ->
vm.memory.getUB(allocations["main.d_res"]!!) shouldBe 17u
vm.memory.getUB(allocations["main.r_res"]!!) shouldBe 9u
}
}
test("subtraction with constant left operand is correct (non-commutative)") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
word sub_res
word div_res
word mod_res
sub start() {
word x = 10
sub_res = 20 - x
div_res = 100 / x
mod_res = 25 % x
}
}
""".trimIndent()
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irSource = virtfile.readText()
val irProgram = IRFileReader().read(irSource)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irSource) { vm ->
vm.memory.getUW(allocations["main.sub_res"]!!) shouldBe 10u
vm.memory.getUW(allocations["main.div_res"]!!) shouldBe 10u
vm.memory.getUW(allocations["main.mod_res"]!!) shouldBe 5u
}
}
test("memory() in array should not crash IRFileWriter") {
val code = """
%zeropage basicsafe
main {
pointer[2] addresses = [ memory("a1", 10, 0), memory("a2", 20, 0) ]
sub start() {
pointer @shared a = addresses[0]
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
val result = compileText(VMTarget(), true, code, outputDir, errors)
withClue(errors.errors.joinToString("\n")) {
result shouldNotBe null
}
}
test("LONG constants and variables should not crash IRFileWriter") {
val code = """
%zeropage basicsafe
main {
const long CL = $12345678
long vl = $87654321
sub start() {
long @shared v1 = CL
long @shared v2 = vl
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
val result = compileText(VMTarget(), true, code, outputDir, errors)
withClue(errors.errors.joinToString("\n")) {
result shouldNotBe null
}
}
test("some dummy support for rombank() rambank() in virtual target") {
val src = """
main {
sub start() {
cx16.r0 = cx16.numbanks()
cx16.rambank(0)
cx16.rombank(0)
}
}
""".trimIndent()
val result = compileText(VMTarget(), false, src, outputDir)
result shouldNotBe null
}
test("typed pointer var init not removed by optimizer when immediately dereferenced") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
uword data = $1234
uword @shared result
sub start() {
result = read_data()
}
sub read_data() -> uword {
long ptr = &main.data
uword val = peekw(ptr)
return val
}
}
""".trimIndent()
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
vm.memory.getUW(allocations["main.result"]!!) shouldBe 0x1234u
}
}
test("ZP string and array initializers use MEMCOPY shadow variables") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
str @zp msg = "hello"
ubyte[5] @zp counts = [10, 20, 30, 40, 50]
uword[3] @zp @nosplit values = [1000, 2000, 3000]
sub start() {
cx16.r0 = values[0]
cx16.r1 = values[1]
cx16.r2 = values[2]
cx16.r3 = counts[0]
cx16.r4 = counts[4]
cx16.r5L = msg[0]
}
}""".trimIndent()
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
irContent shouldContain "main.msg_init_value"
irContent shouldContain "main.counts_init_value"
irContent shouldContain "main.values_init_value"
irContent shouldContain "syscall $1019"
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
val msgAddr = allocations["main.msg"]!!
vm.memory.getString(msgAddr) shouldBe "hello"
val countsAddr = allocations["main.counts"]!!
vm.memory.getUB(countsAddr) shouldBe 10u
vm.memory.getUB(countsAddr + 1u) shouldBe 20u
vm.memory.getUB(countsAddr + 2u) shouldBe 30u
vm.memory.getUB(countsAddr + 3u) shouldBe 40u
vm.memory.getUB(countsAddr + 4u) shouldBe 50u
val valuesAddr = allocations["main.values"]!!
vm.memory.getUW(valuesAddr) shouldBe 1000u
vm.memory.getUW(valuesAddr + 2u) shouldBe 2000u
vm.memory.getUW(valuesAddr + 4u) shouldBe 3000u
}
}
test("power-of-two multiply with -noopt must not crash on shift-count register type") {
// Regression test for the "Wrong shift-count register type" IR-codegen bug:
// a power-of-two multiply lowers to LSLNM whose count register was typed WORD
// instead of BYTE, causing "register rN given multiple types! WORD and BYTE"
// during IR generation. This surfaces with optimizations OFF (the -noopt path).
// The test only checks that compilation succeeds; it currently FAILS
// (throws during the compilation step) until the bug is fixed.
val src = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
uword z = 1234
z = z * 8
}
}
""".trimIndent()
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)
result shouldNotBe null
}
test("indexed array with byte index on 32-bit target canonicalizes to word") {
// Regression for index-register type metadata: LOADX/STOREX/STOREZX index width is WORD
// on 32-bit targets. A BYTE index must be widened to WORD before the indexed op,
// otherwise metadata recomputation would report "register given multiple types".
val src = """
%zeropage basicsafe
%option no_sysinit
main {
word @shared dummy
sub start() {
word[] arr = [1000,2000,3000,4000,5000]
ubyte idx
cx16.r0L = 2
idx = cx16.r0L
word v = arr[idx]
arr[idx] = 9999
arr[idx] = 0
v = arr[idx]
dummy = v
}
}
""".trimIndent()
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)
result shouldNotBe null
val ir = result!!.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir").readText()
val mainStart = ir.substringAfter("<SUB NAME=\"main.start\"").substringBefore("</SUB>")
mainStart shouldContain "ext.b"
mainStart shouldContain "loadx.w"
mainStart shouldContain "storex.w"
mainStart shouldContain "storezx.w"
val irProgram = IRFileReader().read(ir)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(ir) { vm ->
vm.memory.getUW(allocations["main.dummy"]!!) shouldBe 0u
}
}
test("byte array with byte index on 32-bit target") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
ubyte @shared dummy
sub start() {
ubyte[] arr = [10,20,30,40,50]
ubyte idx
cx16.r0L = 1
idx = cx16.r0L
ubyte v = arr[idx]
arr[idx] = 99
dummy = v
}
}
""".trimIndent()
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)
result shouldNotBe null
val ir = result!!.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir").readText()
val irProgram = IRFileReader().read(ir)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(ir) { vm ->
vm.memory.getUB(allocations["main.dummy"]!!) shouldBe 20u
}
}
test("word index on 8-bit target compiles and assembles") {
// Variable word indices are rejected by the semantic checker for regular byte arrays
// on 6502 targets, so this is a smoke test that a constant word index compiles cleanly.
val src = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
ubyte[] arr = [10,20,30,40,50]
uword idx = 3
ubyte v = arr[idx]
arr[idx] = 99
arr[idx] = 0
cx16.r0 = v
}
}
""".trimIndent()
val result = compileText(Cx16Target(), optimize=false, src, outputDir, writeAssembly = true)
result shouldNotBe null
}
test("private_symbols with public override compiles and runs correctly") {
val src = """
%zeropage basicsafe
%option no_sysinit
%option private_symbols
main {
sub start() {
other.doStuff()
}
}
other {
public sub doStuff() {
}
}
""".trimIndent()
val errors = ErrorReporterForTests(throwExceptionAtReportIfErrors = false, keepMessagesAfterReporting = true)
val result = compileText(VMTarget(), true, src, outputDir, errors=errors, writeAssembly = false)
result shouldNotBe null
}
test("word array /= in-place (signed)") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
; const index, const value
word[] arr1 = [-6000, 10000, -5000]
arr1[0] /= 30
; const index, variable value
word[] arr2 = [-6000, 10000, -5000]
word v2 = 50
arr2[1] /= v2
; variable index, const value
word[] arr3 = [-6000, 10000, -5000]
ubyte idx3 = 2
arr3[idx3] /= 25
main.r1 = arr1[0]
main.r2 = arr1[1]
main.r3 = arr1[2]
main.r4 = arr2[0]
main.r5 = arr2[1]
main.r6 = arr2[2]
main.r7 = arr3[0]
main.r8 = arr3[1]
main.r9 = arr3[2]
}
word @shared r1
word @shared r2
word @shared r3
word @shared r4
word @shared r5
word @shared r6
word @shared r7
word @shared r8
word @shared r9
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
// arr1: [-6000/30, 10000, -5000] = [-200, 10000, -5000]
vm.memory.getSW(allocations["main.r1"]!!) shouldBe -200
vm.memory.getSW(allocations["main.r2"]!!) shouldBe 10000
vm.memory.getSW(allocations["main.r3"]!!) shouldBe -5000
// arr2: [-6000, 10000/50, -5000] = [-6000, 200, -5000]
vm.memory.getSW(allocations["main.r4"]!!) shouldBe -6000
vm.memory.getSW(allocations["main.r5"]!!) shouldBe 200
vm.memory.getSW(allocations["main.r6"]!!) shouldBe -5000
// arr3: [-6000, 10000, -5000/25] = [-6000, 10000, -200]
vm.memory.getSW(allocations["main.r7"]!!) shouldBe -6000
vm.memory.getSW(allocations["main.r8"]!!) shouldBe 10000
vm.memory.getSW(allocations["main.r9"]!!) shouldBe -200
}
}
test("non-split word array /= in-place (signed)") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
word[] @nosplit arr = [-6000, 10000, -5000]
arr[0] /= 30
ubyte idx = 2
arr[idx] /= 25
main.r1 = arr[0]
main.r2 = arr[1]
main.r3 = arr[2]
}
word @shared r1
word @shared r2
word @shared r3
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
vm.memory.getSW(allocations["main.r1"]!!) shouldBe -200
vm.memory.getSW(allocations["main.r2"]!!) shouldBe 10000
vm.memory.getSW(allocations["main.r3"]!!) shouldBe -200
}
}
test("float array /= in-place") {
val src = """
%zeropage basicsafe
%option no_sysinit
%import floats
main {
sub start() {
; const index, const value
float[] arr1 = [1.5, 10.0, 2.5]
arr1[1] /= 4.0
; variable index, const value
float[] arr2 = [1.5, 10.0, 2.5]
ubyte idx2 = 2
arr2[idx2] /= 2.0
main.r1 = arr1[0]
main.r2 = arr1[1]
main.r3 = arr1[2]
main.r4 = arr2[0]
main.r5 = arr2[1]
main.r6 = arr2[2]
}
float @shared r1
float @shared r2
float @shared r3
float @shared r4
float @shared r5
float @shared r6
}"""
val errors = ErrorReporterForTests(throwExceptionAtReportIfErrors = false, keepMessagesAfterReporting = true)
val result = compileText(VMTarget(), optimize=false, src, outputDir, errors=errors, writeAssembly = true)
withClue(errors.errors.joinToString("\n")) {
result shouldNotBe null
}
val compiled = result!!
val virtfile = compiled.compilationOptions.outputDir.resolve(compiled.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
// arr1: [1.5, 10.0/4.0, 2.5] = [1.5, 2.5, 2.5]
vm.memory.getFloat(allocations["main.r1"]!!) shouldBe 1.5
vm.memory.getFloat(allocations["main.r2"]!!) shouldBe 2.5
vm.memory.getFloat(allocations["main.r3"]!!) shouldBe 2.5
// arr2: [1.5, 10.0, 2.5/2.0] = [1.5, 10.0, 1.25]
vm.memory.getFloat(allocations["main.r4"]!!) shouldBe 1.5
vm.memory.getFloat(allocations["main.r5"]!!) shouldBe 10.0
vm.memory.getFloat(allocations["main.r6"]!!) shouldBe 1.25
}
}
test("word array %= in-place (signed)") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
; const index, const value
word[] arr1 = [-6000, 10000, -5000]
arr1[0] %= 4000
; variable index, const value
word[] arr2 = [-6000, 10000, -5000]
ubyte idx2 = 1
arr2[idx2] %= 3000
main.r1 = arr1[0]
main.r2 = arr1[1]
main.r3 = arr1[2]
main.r4 = arr2[0]
main.r5 = arr2[1]
main.r6 = arr2[2]
}
word @shared r1
word @shared r2
word @shared r3
word @shared r4
word @shared r5
word @shared r6
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
val irProgram = IRFileReader().read(irContent)
irProgram.st.stripAllPrefixes()
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
// arr1: [-6000%4000, 10000, -5000] = [-2000, 10000, -5000]
vm.memory.getSW(allocations["main.r1"]!!) shouldBe -2000
vm.memory.getSW(allocations["main.r2"]!!) shouldBe 10000
vm.memory.getSW(allocations["main.r3"]!!) shouldBe -5000
// arr2: [-6000, 10000%3000, -5000] = [-6000, 1000, -5000]
vm.memory.getSW(allocations["main.r4"]!!) shouldBe -6000
vm.memory.getSW(allocations["main.r5"]!!) shouldBe 1000
vm.memory.getSW(allocations["main.r6"]!!) shouldBe -5000
}
}
test("asmsub multi-assign with status flag return values generates correct IR") {
val src = """
%option no_sysinit
main {
sub start() {
ubyte v1
bool f1
ubyte v2
bool f2
ubyte v3
bool f3
v1, f1 = testPz()
main.r1 = v1
main.b1 = f1
v2, f2 = testPn()
main.r2 = v2
main.b2 = f2
v3, f3 = testPv()
main.r3 = v3
main.b3 = f3
}
asmsub testPz() -> ubyte @A, bool @Pz {
%asm {{
lda #42
cmp #42 ; Z=1
rts
}}
}
asmsub testPn() -> ubyte @A, bool @Pn {
%asm {{
lda #255 ; N=1
rts
}}
}
asmsub testPv() -> ubyte @A, bool @Pv {
%asm {{
lda #1
clv
rts
}}
}
ubyte @shared r1
ubyte @shared r2
ubyte @shared r3
bool @shared b1
bool @shared b2
bool @shared b3
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
irContent shouldContain "bsteq"
irContent shouldContain "bstneg"
irContent shouldContain "bstvc"
}
test("asmsub multi-assign with a status flag and a register return orders the register read first") {
// The register return must be read before the flag extraction (which uses a
// scratch hardware register), protected by an outer PUSHST/POPST.
val src = """
%option no_sysinit
main {
sub start() {
ubyte v
bool c
v, c = test()
main.r1 = v
main.b1 = c
}
asmsub test() -> ubyte @A, bool @Pc {
%asm {{
lda #42
cmp #42
rts
}}
}
ubyte @shared r1
bool @shared b1
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val lines = virtfile.readText().lines().map { it.trim() }
// one outer PUSHST/POPST pair (only one flag, so no per-flag wrapping)
lines.count { it == "pushst" } shouldBe 1
lines.count { it == "popst" } shouldBe 1
// the register read (loadhr) must come before the flag extraction (roxl)
val loadhrIdx = lines.indexOfFirst { it.startsWith("loadhr") }
val roxlIdx = lines.indexOfFirst { it.startsWith("roxl") }
loadhrIdx shouldBeGreaterThan -1
roxlIdx shouldBeGreaterThan -1
(loadhrIdx < roxlIdx) shouldBe true
}
test("asmsub multi-assign with multiple status flags in one call saves the status around each flag") {
val src = """
%option no_sysinit
main {
sub start() {
ubyte v
bool c
bool z
bool n
v, c, z, n = testFlags()
main.r1 = v
main.b1 = c
main.b2 = z
main.b3 = n
}
asmsub testFlags() -> ubyte @A, bool @Pc, bool @Pz, bool @Pn {
%asm {{
lda #42
cmp #42
rts
}}
}
ubyte @shared r1
bool @shared b1
bool @shared b2
bool @shared b3
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
// 1 outer PUSHST/POPST pair protects the register return read plus the flags across it,
// and each of the three status flags needs its own PUSHST/POPST pair, because the
// branch-based extraction of one flag clobbers the status for the following flags
irContent.lines().count { it.trim() == "pushst" } shouldBe 4
irContent.lines().count { it.trim() == "popst" } shouldBe 4
}
test("union zero-init via memory, write byte and read overlapping word") {
val src = """
main {
union U {
uword w
ubyte b
}
sub start() {
^^U u = memory("u", sizeof(U), 0)
u.w = 0
u.b = $42
cx16.r0 = u.w
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText(), false) { vm ->
vm.memory.getUW(0xff0000u) shouldBe 0x0042u
}
}
test("union copy via pointer dereference") {
val src = """
main {
union U {
uword w
ubyte[2] bytes
}
sub start() {
^^U u1 = memory("u1", sizeof(U), 0)
^^U u2 = memory("u2", sizeof(U), 0)
u1.bytes[0] = $78
u1.bytes[1] = $56
u2^^ = u1^^
cx16.r0 = u2.w
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText(), false) { vm ->
vm.memory.getUW(0xff0000u) shouldBe 0x5678u
}
}
test("union in array of instances") {
val src = """
main {
union U {
uword w
ubyte b
}
U[2] arr
sub start() {
arr[0].b = $42
arr[1].b = $77
cx16.r0 = arr[0].w
cx16.r1 = arr[1].w
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText(), false) { vm ->
vm.memory.getUW(0xff0000u) shouldBe 0x0042u
vm.memory.getUW(0xff0002u) shouldBe 0x0077u
}
}
test("containment check scans arrays larger than 255 elements") {
// Regression test: the IR used a byte-length register for array contains syscalls,
// which crashed for lengths > 255 and produced false negatives on VM/m68k targets.
val src = """
main {
bool @shared bfound
bool @shared bnotfound
bool @shared wfound
bool @shared wnotfound
sub start() {
ubyte[300] @shared barr
barr[260] = 42
bfound = 42 in barr
bnotfound = 99 in barr
word[300] @shared warr
warr[280] = 3000
wfound = 3000 in warr
wnotfound = 9999 in warr
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(virtfile.readText(), false) { vm ->
vm.memory.getUB(allocations["main.bfound"]!!) shouldBe 1u
vm.memory.getUB(allocations["main.bnotfound"]!!) shouldBe 0u
vm.memory.getUB(allocations["main.wfound"]!!) shouldBe 1u
vm.memory.getUB(allocations["main.wnotfound"]!!) shouldBe 0u
}
}
})