mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
1956 lines
66 KiB
Kotlin
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
|
|
}
|
|
}
|
|
|
|
|
|
})
|