mirror of
https://github.com/irmen/prog8.git
synced 2026-10-24 18:02:40 +00:00
344 lines
14 KiB
Kotlin
344 lines
14 KiB
Kotlin
package prog8tests.compiler
|
|
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.engine.spec.tempdir
|
|
import io.kotest.matchers.ints.shouldBeGreaterThan
|
|
import io.kotest.matchers.ints.shouldBeLessThan
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.shouldNotBe
|
|
import io.kotest.matchers.string.shouldContain
|
|
import io.kotest.matchers.string.shouldNotContain
|
|
import io.kotest.matchers.types.instanceOf
|
|
import prog8.code.ast.*
|
|
import prog8.code.target.Cx16Target
|
|
import prog8.code.target.Qemu68kTarget
|
|
import prog8.code.target.VMTarget
|
|
import prog8.intermediate.IRFileReader
|
|
import prog8.vm.VmRunner
|
|
import prog8.vm.VmVariableAllocator
|
|
import prog8tests.helpers.compileText
|
|
import prog8tests.helpers.simulate
|
|
import kotlin.io.path.readText
|
|
|
|
class TestDefers : FunSpec({
|
|
val outputDir = tempdir().toPath()
|
|
|
|
test("defer syntactic sugaring") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void test()
|
|
}
|
|
|
|
sub test() -> uword {
|
|
defer {
|
|
cx16.r0++
|
|
cx16.r1++
|
|
}
|
|
|
|
if cx16.r0==0 {
|
|
defer cx16.r1++
|
|
}
|
|
|
|
if cx16.r0==0
|
|
return cx16.r0+cx16.r1
|
|
defer cx16.r2++
|
|
return 999
|
|
}
|
|
}"""
|
|
val result = compileText(Cx16Target(), optimize = true, src, outputDir, writeAssembly = true)!!
|
|
val main = result.codegenAst!!.allBlocks().single { it.name == "p8b_main" }
|
|
val sub = main.children[1] as PtSub
|
|
sub.scopedName shouldBe "p8b_main.p8s_test"
|
|
|
|
// check the desugaring of the defer statements (allow extra push nodes for program-wide stack)
|
|
sub.children[0] shouldBe instanceOf<PtSubSignature>()
|
|
sub.children.filterIsInstance<PtVariable>().single { it.name=="p8v_prog8_defers_mask" } shouldNotBe null
|
|
|
|
val topDeferEnables = sub.children.filterIsInstance<PtAugmentedAssign>().filter { it.operator=="|=" && it.target.identifier?.name=="p8b_main.p8s_test.p8v_prog8_defers_mask" }
|
|
topDeferEnables.size shouldBe 2
|
|
topDeferEnables[0].value.asConstInteger() shouldBe 4
|
|
topDeferEnables[1].value.asConstInteger() shouldBe 1
|
|
|
|
// defer inside first if (conditional) - skip overflow check if present
|
|
val firstIf = sub.children.filterIsInstance<PtIfElse>().first { it.ifScope.children.any { c -> c is PtAugmentedAssign && c.target.identifier?.name?.contains("prog8_defers_mask")==true } }
|
|
val deferInIf = firstIf.ifScope.children.filterIsInstance<PtAugmentedAssign>().single()
|
|
deferInIf.operator shouldBe "|="
|
|
deferInIf.target.identifier?.name shouldBe "p8b_main.p8s_test.p8v_prog8_defers_mask"
|
|
deferInIf.value.asConstInteger() shouldBe 2
|
|
|
|
// second if with complex return
|
|
val ifelse = sub.children.filterIsInstance<PtIfElse>().last()
|
|
val ifscope = ifelse.ifScope.children[0] as PtNodeGroup
|
|
// group contains pushw, decSp, handler call, return(popw)
|
|
val pushInGroup = ifscope.children.filterIsInstance<PtFunctionCall>().single { it.name=="pushw" }
|
|
pushInGroup shouldNotBe null
|
|
val deferInGroup = ifscope.children.filterIsInstance<PtFunctionCall>().single { it.name=="p8b_main.p8s_test.p8s_prog8_invoke_defers" }
|
|
deferInGroup shouldNotBe null
|
|
val retInGroup = ifscope.children.filterIsInstance<PtReturn>().single()
|
|
(retInGroup.children.single() as PtFunctionCall).name shouldBe "popw"
|
|
|
|
// ending pop+handler+return before handler sub
|
|
val endingCalls = sub.children.filterIsInstance<PtFunctionCall>().filter { it.name=="p8b_main.p8s_test.p8s_prog8_invoke_defers" }
|
|
endingCalls.isNotEmpty() shouldBe true
|
|
// should have a decSp before ending call (program-wide pop)
|
|
val hasDecSpBeforeEnding = sub.children.filterIsInstance<PtAugmentedAssign>().any { it.operator=="-=" && it.target.identifier?.name?.contains("defer_sp")==true }
|
|
hasDecSpBeforeEnding shouldBe true
|
|
|
|
val handler = sub.children.filterIsInstance<PtSub>().single { it.name=="p8s_prog8_invoke_defers" }
|
|
handler shouldNotBe null
|
|
}
|
|
|
|
test("defer blocks prevent tail call optimization") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
void test()
|
|
}
|
|
|
|
sub test() {
|
|
defer {
|
|
cx16.r0 = 1
|
|
}
|
|
return other()
|
|
}
|
|
|
|
sub other() {
|
|
cx16.r1 = 2
|
|
}
|
|
}"""
|
|
// We compile for Cx16Target because tail call optimization (TCO) is a 6502 backend optimization
|
|
// (actually it's in StatementReorderer which runs before codegen, but it targets JMP vs JSR)
|
|
val result = compileText(Cx16Target(), optimize = true, src, outputDir, writeAssembly = true)!!
|
|
val main = result.codegenAst!!.allBlocks().single { it.name == "p8b_main" }
|
|
val testSub = main.children.filterIsInstance<PtSub>().find { it.scopedName == "p8b_main.p8s_test" }!!
|
|
|
|
// If TCO happened, we would see a PtJump to other instead of a PtFunctionCall to other.
|
|
// But with defer, it should remain a PtFunctionCall (or rather, it's inside a node group that calls invoke_defers)
|
|
|
|
// Let's inspect the children of testSub.
|
|
// It should have: signature, mask var, initialize mask, set mask bit, the call to other, call to invoke_defers, return.
|
|
|
|
fun collectCalls(node: PtNode): List<PtFunctionCall> {
|
|
val calls = mutableListOf<PtFunctionCall>()
|
|
if (node is PtFunctionCall) calls.add(node)
|
|
node.children.forEach { calls.addAll(collectCalls(it)) }
|
|
return calls
|
|
}
|
|
|
|
val callToOther = collectCalls(testSub).find { it.name == "p8b_main.p8s_other" }
|
|
callToOther shouldNotBe null
|
|
|
|
// Verify it is NOT a PtJump (which would be used for tail call optimization)
|
|
// Actually, TCO in StatementReorderer converts return other() into a Goto.
|
|
// In PtSub, a Goto becomes a PtJump.
|
|
|
|
testSub.children.any { it is PtJump && (it.target as? PtIdentifier)?.name == "p8b_main.p8s_other" } shouldBe false
|
|
}
|
|
|
|
test("defer program-wide unwind via sys.exit") {
|
|
val src = """
|
|
main {
|
|
uword @shared result = 0
|
|
|
|
sub start() {
|
|
defer result = result * 10 + 3
|
|
defer result = result * 10 + 4
|
|
helper()
|
|
; should not reach here if program-wide unwind works via sys.exit
|
|
result = 99
|
|
}
|
|
|
|
sub helper() {
|
|
defer result = result * 10 + 1
|
|
defer result = result * 10 + 2
|
|
sys.exit(0)
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), optimize = true, src, outputDir, writeAssembly = true)!!
|
|
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
|
|
val irSrc = virtfile.readText()
|
|
// program-wide stack must be present
|
|
irSrc shouldContain "prog8_defer"
|
|
irSrc shouldContain "defer_unwind_all"
|
|
val irProgram = IRFileReader().read(irSrc)
|
|
irProgram.st.stripAllPrefixes()
|
|
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
|
|
VmRunner().runAndTestProgram(irSrc) { vm ->
|
|
// helper defers 2,1 then start defers 4,3 -> ((0*10+2)*10+1)*10+4)*10+3 = 2143
|
|
// if only local unwind, result would be 21 (only helper) or 99 (if start continued)
|
|
vm.memory.getUW(allocations["main.result"]!!) shouldBe 2143u
|
|
}
|
|
}
|
|
|
|
test("defer with multi-value return of non-simple expressions") {
|
|
val src = """
|
|
main {
|
|
uword @shared ra = 0
|
|
uword @shared rb = 0
|
|
uword @shared rc = 0
|
|
uword @shared x = 11
|
|
uword @shared y = 22
|
|
uword @shared z = 33
|
|
|
|
sub triple() -> uword, uword, uword {
|
|
defer x += 1
|
|
return x, y, z
|
|
}
|
|
|
|
sub start() {
|
|
uword a, b, c
|
|
a, b, c = triple()
|
|
ra = a
|
|
rb = b
|
|
rc = c
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), optimize = false, src, outputDir, writeAssembly = true)!!
|
|
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
|
|
val irSrc = virtfile.readText()
|
|
// variable identifiers in the return are non-simple: return values are stack-saved across defers
|
|
irSrc shouldContain "push"
|
|
val irProgram = IRFileReader().read(irSrc)
|
|
irProgram.st.stripAllPrefixes()
|
|
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
|
|
VmRunner().runAndTestProgram(irSrc) { vm ->
|
|
// regression: the stack order for saved return values used to be reversed, swapping the first
|
|
// value with the others (e.g. dos.read_file() returned the data pointer as second value)
|
|
vm.memory.getUW(allocations["main.ra"]!!) shouldBe 11u
|
|
vm.memory.getUW(allocations["main.rb"]!!) shouldBe 22u
|
|
vm.memory.getUW(allocations["main.rc"]!!) shouldBe 33u
|
|
}
|
|
}
|
|
|
|
test("defer with multi-value return executed on 6502") {
|
|
val machine = compileText(Cx16Target(), false, $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $2000
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&uword res_a = $0200
|
|
&uword res_b = $0202
|
|
&uword res_c = $0204
|
|
uword @shared x = 11
|
|
uword @shared y = 22
|
|
uword @shared z = 33
|
|
sub triple() -> uword, uword, uword {
|
|
defer x += 1
|
|
return x, y, z
|
|
}
|
|
sub start() {
|
|
uword a, b, c
|
|
a, b, c = triple()
|
|
res_a = a
|
|
res_b = b
|
|
res_c = c
|
|
poweroff = 1
|
|
}
|
|
}""".trimIndent(), outputDir)!!.simulate()
|
|
machine.assertMemory(0x0200, 11)
|
|
machine.assertMemory(0x0202, 22)
|
|
machine.assertMemory(0x0204, 33)
|
|
}
|
|
|
|
test("defer with multi-value float return assembly order on 6502") {
|
|
// Runtime simulation of cx16 float code is not possible with ksim65 because the
|
|
// floats helpers (MOVFM/MOVMF) are extsubs to KERNAL ROM routines ($fe5c/$fe66).
|
|
// Instead, verify the generated assembly order: the float must be loaded/restored
|
|
// before the uword value, because MOVFM/popFAC clobber A/Y.
|
|
val src = """
|
|
%import floats
|
|
main {
|
|
uword @shared x = 11
|
|
float @shared f = 1.5
|
|
sub pairf() -> uword, float {
|
|
defer x += 1
|
|
return x, f
|
|
}
|
|
sub start() {
|
|
uword a
|
|
float b
|
|
a, b = pairf()
|
|
}
|
|
}"""
|
|
val result = compileText(Cx16Target(), optimize = false, src, outputDir, writeAssembly = true)!!
|
|
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
|
|
val asm = asmFile.readText().lines().map { it.trim() }
|
|
val pairfStart = asm.indexOfFirst { it == "p8s_pairf\t.proc" }
|
|
val pairfEnd = asm.drop(pairfStart).indexOfFirst { it == "rts" } + pairfStart
|
|
val pairfBody = asm.subList(pairfStart, pairfEnd + 1)
|
|
// find the relative order of float pop and uword pop in the defer path
|
|
val popFacIdx = pairfBody.indexOfFirst { it.contains("jsr") && it.contains("popFAC") }
|
|
val plyIdx = pairfBody.indexOfFirst { it == "ply" }
|
|
popFacIdx shouldBeGreaterThan -1
|
|
plyIdx shouldBeGreaterThan -1
|
|
popFacIdx shouldBeLessThan plyIdx // float restored before uword, so AY isn't clobbered
|
|
}
|
|
|
|
test("defer program-wide no overhead when unused") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
cx16.r0++
|
|
}
|
|
}"""
|
|
val result = compileText(VMTarget(), optimize = true, src, outputDir, writeAssembly = true)!!
|
|
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
|
|
val irSrc = virtfile.readText()
|
|
irSrc shouldNotContain "prog8_defer"
|
|
irSrc shouldNotContain "defer_unwind_all"
|
|
}
|
|
|
|
test("defer program-wide unwind via sys.exit on cx16") {
|
|
val src = """
|
|
%zeropage basicsafe
|
|
%option no_sysinit
|
|
main {
|
|
uword @shared result = 0
|
|
sub start() {
|
|
defer result = result * 10 + 3
|
|
defer result = result * 10 + 4
|
|
helper()
|
|
}
|
|
sub helper() {
|
|
defer result = result * 10 + 1
|
|
defer result = result * 10 + 2
|
|
sys.exit(0)
|
|
}
|
|
}"""
|
|
val result = compileText(Cx16Target(), optimize = true, src, outputDir, writeAssembly = true)!!
|
|
val prog = result.codegenAst!!
|
|
prog.allBlocks().any { it.name.contains("prog8_defer") } shouldBe true
|
|
prog.allBlocks().flatMap<PtBlock, PtNode> { it.children }.filterIsInstance<PtVariable>().any { it.name.contains("defer_sp") } shouldBe true
|
|
prog.allBlocks().flatMap<PtBlock, PtNode> { it.children }.filterIsInstance<PtSub>().any { it.name.contains("defer_unwind_all") } shouldBe true
|
|
val allSubs = prog.allBlocks().flatMap<PtBlock, PtNode> { it.children }.filterIsInstance<PtSub>()
|
|
val subsNames = allSubs.map { it.name } + allSubs.flatMap { it.children }.filterIsInstance<PtSub>().map { it.name }
|
|
subsNames.any { it.contains("prog8_invoke_defers") } shouldBe true
|
|
}
|
|
|
|
test("defer program-wide structure on qemu68k without vasm") {
|
|
val src = """
|
|
main {
|
|
uword @shared result = 0
|
|
sub start() {
|
|
defer result = result * 10 + 3
|
|
defer result = result * 10 + 4
|
|
helper()
|
|
}
|
|
sub helper() {
|
|
defer result = result * 10 + 1
|
|
defer result = result * 10 + 2
|
|
sys.exit(0)
|
|
}
|
|
}"""
|
|
val result = compileText(Qemu68kTarget(), optimize = true, src, outputDir, writeAssembly = true, assemble = false)!!
|
|
val prog = result.codegenAst!!
|
|
prog.allBlocks().any { it.name.contains("prog8_defer") } shouldBe true
|
|
prog.allBlocks().flatMap<PtBlock, PtNode> { it.children }.filterIsInstance<PtVariable>().any { it.name.contains("defer_sp") } shouldBe true
|
|
val subs = prog.allBlocks().flatMap<PtBlock, PtNode> { it.children }.filterIsInstance<PtSub>().map { it.name }
|
|
subs.any { it.contains("defer_unwind_all") } shouldBe true
|
|
subs.any { it.contains("prog8_invoke_defers") } shouldBe true
|
|
}
|
|
})
|