mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 20:02:27 +00:00
1393 lines
46 KiB
Kotlin
1393 lines
46 KiB
Kotlin
package prog8tests.compiler
|
|
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.engine.spec.tempdir
|
|
import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.shouldNotBe
|
|
import io.kotest.matchers.string.shouldContain
|
|
import io.kotest.matchers.string.shouldNotContain
|
|
import prog8.code.target.C64Target
|
|
import prog8.code.target.Qemu68kTarget
|
|
import prog8.intermediate.IRFileReader
|
|
import prog8.intermediate.IRStStaticVariable
|
|
import prog8tests.helpers.ErrorReporterForTests
|
|
import prog8tests.helpers.compileText
|
|
|
|
// M68K stack-frame end-to-end tests:
|
|
// stack-frame locals for frameable leaf subroutines on m68k targets.
|
|
class TestStackFrameLocals : FunSpec({
|
|
|
|
fun compile(src: String, target: prog8.code.core.ICompilationTarget = Qemu68kTarget()): Pair<List<String>, String> {
|
|
val outputDir = tempdir().toPath()
|
|
compileText(target, optimize = false, src, outputDir, writeAssembly = true, assemble = false)
|
|
val asmFile = outputDir.toFile().listFiles()!!.single { it.name.endsWith(".asm") }
|
|
val irFile = outputDir.toFile().listFiles()!!.singleOrNull { it.name.endsWith(".p8ir") }
|
|
return asmFile.readText().lines().map { it.trim() } to (irFile?.readText() ?: "")
|
|
}
|
|
|
|
// the assembly lines of a single subroutine, using the subroutine boundary markers the
|
|
// asm peephole optimizer relies on
|
|
fun subAssembly(lines: List<String>, subLabel: String): List<String> {
|
|
val start = lines.indexOfFirst { it.startsWith("; ---- Subroutine:") && it.contains(subLabel) }
|
|
require(start >= 0) { "no assembly found for subroutine $subLabel" }
|
|
val end = lines.indexOfFirst { it.startsWith("; End of subroutine:") && it.contains(subLabel) }
|
|
return lines.subList(start, end)
|
|
}
|
|
|
|
test("frameable leaf subroutine gets an a5 frame") {
|
|
val src = """
|
|
main {
|
|
ubyte gv = 3
|
|
sub leaf() {
|
|
ubyte a
|
|
word b
|
|
ubyte[4] arr
|
|
a = gv
|
|
b = a * 2
|
|
arr[3] = a
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
// clean locals grouped first: arr at -4..-1, b at -6..-5, a at -7 -> frame size 8
|
|
lines.any { it == "link a5,#-8" } shouldBe true
|
|
lines.count { it == "unlk a5" } shouldBe 1
|
|
val unlkIdx = lines.indexOf("unlk a5")
|
|
lines[unlkIdx + 1] shouldBe "rts"
|
|
// accesses lowered to a5 displacements
|
|
lines.any { "-7(a5)" in it } shouldBe true
|
|
lines.any { "-6(a5)" in it } shouldBe true
|
|
lines.any { "-1(a5)" in it } shouldBe true // arr[3]
|
|
// locals are no longer static storage
|
|
lines.none { "p8v_a:" in it || "p8v_arr:" in it || "p8v_b:" in it } shouldBe true
|
|
// IR carries the new syntax and frame size, and is reloadable
|
|
ir.contains("frame:-7") shouldBe true
|
|
ir.contains("FRAMESIZE=\"8\"") shouldBe true
|
|
val reloaded = IRFileReader().read(ir)
|
|
reloaded.allSubs().first { it.label.endsWith("p8s_leaf") }.frameSize shouldBe 8
|
|
}
|
|
|
|
test("subroutine with parameters passes them on the stack") {
|
|
val src = """
|
|
main {
|
|
sub withp(ubyte a, word b, long c) -> long {
|
|
return b + c + a
|
|
}
|
|
sub start() {
|
|
g = lsb(withp(1, 2, 3))
|
|
}
|
|
ubyte g
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
// incoming-only frame: no locals, but the parameters need a frame pointer
|
|
lines.any { it == "link a5,#0" } shouldBe true
|
|
lines.any { it == "unlk a5" } shouldBe true
|
|
// word-packed slots: long at 8, word slot at 12, byte slot at 14 with the byte at 14
|
|
lines.any { it.contains("14(a5)") } shouldBe true // ubyte at slot 14 base byte
|
|
lines.any { it.contains("12(a5)") } shouldBe true // word at slot 12
|
|
lines.any { it.contains("8(a5)") } shouldBe true // long at slot 8
|
|
// parameters are no longer static storage
|
|
lines.none { "p8v_a:" in it || "p8v_b:" in it || "p8v_c:" in it } shouldBe true
|
|
// the caller pushes every argument into its own slot (byte: single one-instruction
|
|
// push carrying a "pushed as a word" note), then pops the whole area
|
|
lines.count { it.startsWith("move.b ") && it.contains(",-(sp)") && it.contains("pushed as a word") } shouldBe 1
|
|
lines.count { it.startsWith("move.w ") && it.contains(",-(sp)") } shouldBe 1
|
|
lines.count { it == "subq.l #2,sp" } shouldBe 0
|
|
// long/pointer slots push with a single move.l ...,-(sp), same shape as PUSH ops elsewhere,
|
|
// so only require at least the one argument push (exact isolation is covered in TestStackFrameEmission)
|
|
lines.count { it.startsWith("move.l ") && it.contains(",-(sp)") } shouldBeGreaterThanOrEqual 1
|
|
lines.any { it == "addq.l #8,sp" } shouldBe true
|
|
// IR carries the incoming area size and the frame-slot argument locations
|
|
ir.contains("INCOMING=\"8\"") shouldBe true
|
|
ir.contains("frame:14") shouldBe true
|
|
val reloaded = IRFileReader().read(ir)
|
|
val sub = reloaded.allSubs().first { it.label.endsWith("p8s_withp") }
|
|
sub.frameSize shouldBe 0
|
|
sub.incomingSize shouldBe 8
|
|
}
|
|
|
|
test("many parameters use one cleanup for the whole argument area") {
|
|
val src = """
|
|
main {
|
|
sub five(ubyte a, uword b, long c, ubyte d, uword e) -> long {
|
|
return a + b + c + d + e
|
|
}
|
|
sub start() {
|
|
g = lsb(five(1, 2, 3, 4, 5))
|
|
}
|
|
ubyte g
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
// no byte slot reservations; byte pushes use a single move.b predecrement
|
|
lines.count { it == "subq.l #2,sp" } shouldBe 0
|
|
lines.any { it == "lea 12(sp),sp" } shouldBe true // 12 bytes: the addq form maxes out at 8
|
|
// 5 parameters, word-packed slots, accumulated right-to-left: byte slot at 18,
|
|
// word slot at 16, long slot at 12, byte slot at 10, word slot at 8
|
|
lines.any { it.contains("18(a5)") } shouldBe true // ubyte at slot 18 base byte
|
|
lines.any { it.contains("16(a5)") } shouldBe true // uword at slot 16
|
|
lines.any { it.contains("12(a5)") } shouldBe true // long at slot 12
|
|
ir.contains("INCOMING=\"12\"") shouldBe true
|
|
}
|
|
|
|
test("subroutine that calls another is framed when it is not recursive") {
|
|
val src = """
|
|
main {
|
|
ubyte gv = 2
|
|
sub inner() { gv += 1 }
|
|
sub caller() {
|
|
ubyte a
|
|
inner()
|
|
a = gv
|
|
}
|
|
sub start() {
|
|
caller()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
lines.any { it == "link a5,#-2" } shouldBe true
|
|
lines.none { "p8v_a:" in it } shouldBe true
|
|
ir.contains("FRAMESIZE=\"2\"") shouldBe true
|
|
}
|
|
|
|
test("recursive subroutine gets a frame with per-activation virtual registers") {
|
|
// slice 3: a subroutine on a call-graph cycle moves its virtual registers into the frame,
|
|
// so two live activations no longer share the flat program-static regfile
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> long {
|
|
if n < 2
|
|
return n
|
|
return rec(n-1) + rec(n-2)
|
|
}
|
|
ubyte @shared g = 1
|
|
sub start() {
|
|
g = lsb(rec(10))
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
val rec = subAssembly(lines, "p8b_main.p8s_rec")
|
|
rec.any { it.startsWith("link ") } shouldBe true
|
|
rec.any { it == "unlk a5" } shouldBe true
|
|
// its parameter arrives in the incoming argument area, its temporary value in the frame
|
|
ir.contains("VREGSLOTS=") shouldBe true
|
|
rec.any { it.contains("(a5)") && it.startsWith("move") } shouldBe true
|
|
// the recursive subroutine's own body never touches the static register file
|
|
rec.any { it.contains("p8_regfile") } shouldBe false
|
|
}
|
|
|
|
test("mutually recursive subroutines both get frames") {
|
|
val src = """
|
|
main {
|
|
sub ping(ubyte n) -> long {
|
|
if n == 0
|
|
return 0
|
|
return pong(n-1)
|
|
}
|
|
sub pong(ubyte n) -> long {
|
|
if n == 0
|
|
return 0
|
|
return ping(n-1)
|
|
}
|
|
sub start() {
|
|
g = lsb(ping(4))
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val ping = subAssembly(lines, "p8b_main.p8s_ping")
|
|
val pong = subAssembly(lines, "p8b_main.p8s_pong")
|
|
ping.any { it.startsWith("link ") } shouldBe true
|
|
pong.any { it.startsWith("link ") } shouldBe true
|
|
// both keep their state per-activation, so neither uses the static register file
|
|
ping.any { it.contains("p8_regfile") } shouldBe false
|
|
pong.any { it.contains("p8_regfile") } shouldBe false
|
|
}
|
|
|
|
test("indirectly called subroutine keeps the static path") {
|
|
val src = """
|
|
main {
|
|
sub t1() { g = 1 }
|
|
sub t2() { g = 2 }
|
|
sub start() {
|
|
on 1 call (
|
|
t1,
|
|
t2
|
|
)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
// t1/t2 are dispatch-table targets: they could be re-entered through the indirect call
|
|
lines.any { it.startsWith("link ") } shouldBe false
|
|
}
|
|
|
|
test("defer-referenced local stays static, deferring sub with parameters keeps the static convention") {
|
|
val src = """
|
|
main {
|
|
sub leaf() {
|
|
ubyte a = 1
|
|
defer a += 1
|
|
}
|
|
sub parm(ubyte x) {
|
|
defer x += 1
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
parm(1)
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
// the defer handler is emitted at block scope and reads the parent local, so that local
|
|
// keeps static storage (§11 option 2)
|
|
lines.any { "p8s_leaf.p8v_a:" in it } shouldBe true
|
|
// a defer body that reads a parameter keeps the whole subroutine on the static convention
|
|
lines.any { "p8s_parm.p8v_x:" in it } shouldBe true
|
|
}
|
|
|
|
test("address-taken local stays static, rest of the frame still works") {
|
|
val src = """
|
|
main {
|
|
sub leaf() {
|
|
ubyte x
|
|
^^ubyte p = &x
|
|
p^^ = 42
|
|
x += p^^
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
// x is address-taken: keeps its static storage; p (a pointer scalar) gets framed
|
|
lines.any { "p8v_x:" in it } shouldBe true
|
|
lines.any { it == "link a5,#-4" } shouldBe true
|
|
lines.any { "-4(a5)" in it } shouldBe true
|
|
}
|
|
|
|
test("@shared local keeps static storage even without an escaping address") {
|
|
val src = """
|
|
main {
|
|
sub leaf() {
|
|
ubyte @shared sharedx
|
|
ubyte normal
|
|
sharedx = 1
|
|
normal = sharedx
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
// sharedx is not address-taken and no inline assembly mentions it, yet the @shared
|
|
// tag pins it to program-static storage; normal is still framed
|
|
lines.any { "p8v_sharedx:" in it } shouldBe true
|
|
lines.none { "p8v_normal:" in it } shouldBe true
|
|
lines.any { it == "link a5,#-2" } shouldBe true
|
|
lines.any { "-1(a5)" in it } shouldBe true
|
|
ir.contains("shared=true") shouldBe true
|
|
val reloaded = IRFileReader().read(ir)
|
|
val sharedVar = reloaded.st.lookup("p8b_main.p8s_leaf.p8v_sharedx") as IRStStaticVariable
|
|
sharedVar.shared shouldBe true
|
|
reloaded.st.lookup("p8b_main.p8s_leaf.p8v_normal") shouldBe null
|
|
}
|
|
|
|
test("defer-referenced subroutine is not framed") {
|
|
val src = """
|
|
main {
|
|
sub leaf() {
|
|
ubyte a = 1
|
|
defer a += 1
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
// the defer body reads the parent local from a block-scope handler, so that local keeps
|
|
// static storage; the subroutine itself may still get a frame for its per-activation
|
|
// defer mask, but the defer-referenced variable must never move
|
|
lines.any { "p8s_leaf.p8v_a:" in it } shouldBe true
|
|
}
|
|
|
|
test("oversized local array keeps static storage while other locals are framed") {
|
|
val src = """
|
|
main {
|
|
sub leaf() {
|
|
ubyte small
|
|
ubyte[17000] big
|
|
small = 1
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
lines.any { it == "link a5,#-2" } shouldBe true // only `small` is framed
|
|
lines.any { "p8v_big" in it } shouldBe true // big array remains static
|
|
}
|
|
|
|
test("slice 3: tail call out of a framed subroutine becomes a call and a return") {
|
|
// a framed subroutine cannot branch out to another subroutine: its `link` moved the stack
|
|
// pointer, so the other subroutine's `rts` would return into the local area
|
|
val src = """
|
|
main {
|
|
sub target(ubyte n) -> ubyte {
|
|
return n + 1
|
|
}
|
|
sub caller(ubyte n) -> ubyte {
|
|
ubyte local = n
|
|
if local == 0 {
|
|
return 0
|
|
}
|
|
return target(local)
|
|
}
|
|
sub start() {
|
|
g = caller(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
val callerAsm = subAssembly(lines, "p8b_main.p8s_caller")
|
|
callerAsm.any { it.startsWith("link ") } shouldBe true
|
|
// no outgoing branch to another subroutine is left inside the frame
|
|
callerAsm.any { it.startsWith("bra") } shouldBe false
|
|
callerAsm.any { it.startsWith("bsr") } shouldBe true
|
|
ir.contains("call p8b_main.p8s_target") shouldBe true
|
|
}
|
|
|
|
test("conditional goto to a subroutine out of a framed subroutine becomes an inverted branch over a tail call") {
|
|
// the not-taken path must skip the tail call and keep using the frame: the branch
|
|
// is inverted over a call + return, and the fall-through code moves into a skip chunk
|
|
val src = """
|
|
main {
|
|
ubyte @shared result
|
|
sub t1() {
|
|
result += 10
|
|
}
|
|
sub jumper(ubyte n) {
|
|
ubyte local
|
|
local = n
|
|
if local != 0 {
|
|
goto t1
|
|
}
|
|
result += 1
|
|
}
|
|
sub start() {
|
|
result = 0
|
|
jumper(1)
|
|
jumper(0)
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
val jumperAsm = subAssembly(lines, "p8b_main.p8s_jumper")
|
|
jumperAsm.any { it.startsWith("link ") } shouldBe true
|
|
// no conditional branch directly targeting the other subroutine remains in the frame
|
|
// (matching the entry label means the callee's rts would corrupt the machine stack)
|
|
jumperAsm.filter { Regex("b(?!sr)\\w+\\s+p8b_main\\.p8s_t1$").containsMatchIn(it) } shouldBe emptyList()
|
|
// inverted branch over the tail call: bstne t1 became bsteq <skip>, followed by bsr + rts
|
|
val tailSkipLabel = jumperAsm.first { Regex("b\\w+\\s+\\S*tailskip").containsMatchIn(it) }
|
|
tailSkipLabel.isEmpty() shouldBe false
|
|
jumperAsm.any { it == "bsr p8b_main.p8s_t1" } shouldBe true
|
|
ir.contains("call p8b_main.p8s_t1") shouldBe true
|
|
// the not-taken path resumes after the tail call, still inside the frame
|
|
val skipIdx = jumperAsm.indexOfFirst { Regex("\\S*tailskip\\d+:").matches(it) }
|
|
(skipIdx > jumperAsm.indexOf("bsr p8b_main.p8s_t1")) shouldBe true
|
|
}
|
|
|
|
test("conditional goto inside a repeat loop gets the skip chunk inside the loop body") {
|
|
val src = """
|
|
main {
|
|
ubyte @shared result
|
|
sub t1() {
|
|
result += 10
|
|
}
|
|
sub jumper(ubyte n) {
|
|
ubyte local
|
|
local = n
|
|
repeat 2 {
|
|
if local != 0 {
|
|
goto t1
|
|
}
|
|
}
|
|
result += 1
|
|
}
|
|
sub start() {
|
|
result = 0
|
|
jumper(1)
|
|
jumper(0)
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
val jumperAsm = subAssembly(lines, "p8b_main.p8s_jumper")
|
|
jumperAsm.any { it.startsWith("link ") } shouldBe true
|
|
jumperAsm.filter { Regex("b(?!sr)\\w+\\s+p8b_main\\.p8s_t1$").containsMatchIn(it) } shouldBe emptyList()
|
|
jumperAsm.any { it.startsWith("dbra d7,") } shouldBe true
|
|
jumperAsm.any { it == "bsr p8b_main.p8s_t1" } shouldBe true
|
|
ir.contains("call p8b_main.p8s_t1") shouldBe true
|
|
}
|
|
|
|
test("register-form conditional goto out of a framed subroutine inverts with swapped operands") {
|
|
val src = """
|
|
main {
|
|
ubyte @shared result
|
|
ubyte @shared limit
|
|
sub t1() {
|
|
result += 10
|
|
}
|
|
sub jumper(ubyte n) {
|
|
ubyte local
|
|
local = n
|
|
if local > limit {
|
|
goto t1
|
|
}
|
|
result += 1
|
|
}
|
|
sub start() {
|
|
result = 0
|
|
limit = 5
|
|
jumper(9) ; taken: result=10
|
|
jumper(2) ; not taken: result=11
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
val jumperAsm = subAssembly(lines, "p8b_main.p8s_jumper")
|
|
jumperAsm.any { it.startsWith("link ") } shouldBe true
|
|
// bgtr t1 became bger(swap) -> the inverted condition branches over the tail call
|
|
jumperAsm.filter { Regex("b(?!sr)\\w+\\s+p8b_main\\.p8s_t1$").containsMatchIn(it) } shouldBe emptyList()
|
|
jumperAsm.count { it.startsWith("bhs") && Regex("\\S*tailskip").containsMatchIn(it) } shouldBe 1
|
|
jumperAsm.any { it == "bsr p8b_main.p8s_t1" } shouldBe true
|
|
ir.contains("call p8b_main.p8s_t1") shouldBe true
|
|
}
|
|
|
|
test("copy-in parameter that the body writes keeps its static cell as the live value") {
|
|
// §6.1: the entry-time copy-in turns the static cell into the parameter's live storage;
|
|
// the body's writes must land there so external references observe the current value
|
|
val src = """
|
|
main {
|
|
ubyte @shared out
|
|
sub parm(ubyte x) {
|
|
x = 99
|
|
defer out = x
|
|
}
|
|
sub start() {
|
|
parm(1)
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val parm = subAssembly(lines, "p8b_main.p8s_parm")
|
|
parm.any { it == "link a5,#0" } shouldBe true
|
|
// the entry-time copy-in is still performed
|
|
parm.any { it.startsWith("move.b 8(a5),") } shouldBe true
|
|
// the body's write goes to the static cell, not to the incoming frame slot
|
|
parm.any { it.contains("move.b") && it.contains("p8b_main.p8s_parm.p8v_x") && it.contains("#99") } shouldBe true
|
|
// the defer handler reads the static cell, so it observes the body's written value
|
|
val defers = subAssembly(lines, "p8s_prog8_invoke_defers")
|
|
defers.any { it.contains("move.b") && it.contains("p8b_main.p8s_parm.p8v_x") } shouldBe true
|
|
// the static cell is kept in BSS
|
|
lines.any { "p8b_main.p8s_parm.p8v_x:" in it } shouldBe true
|
|
}
|
|
|
|
test("slice 3: recursive subroutine with an address-taken local is rejected") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
ubyte buf = 1
|
|
pointer p = &buf
|
|
if n == 0 {
|
|
return 1
|
|
}
|
|
if p != 0 {
|
|
return rec(n-1)
|
|
}
|
|
return rec(n-1)
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = true, assemble = false, errors = errors) shouldBe null
|
|
errors.errors.size shouldBe 1
|
|
errors.errors[0] shouldContain "shares data between activations"
|
|
errors.errors[0] shouldContain "address is taken"
|
|
}
|
|
|
|
test("slice 3: recursive subroutine that keeps static storage is rejected, other targets are not") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
ubyte[4] buf = [1, 2, 3, 4]
|
|
buf[0] = n
|
|
if n == 0 {
|
|
return buf[0]
|
|
}
|
|
return rec(n-1) + buf[0]
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = true, assemble = false, errors = errors) shouldBe null
|
|
errors.errors.size shouldBe 1
|
|
errors.errors[0] shouldContain "shares data between activations"
|
|
errors.errors[0] shouldContain "static initializer"
|
|
|
|
// the same program compiles unchanged for a target without stack frames
|
|
val otherErrors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(C64Target(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = false, assemble = false, errors = otherErrors) shouldNotBe null
|
|
otherErrors.errors.size shouldBe 0
|
|
}
|
|
|
|
test("recursion warning is only emitted for targets without stack frames") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
if n == 0 {
|
|
return 0
|
|
}
|
|
return rec(n-1) + 1
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val m68kErrors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = false, assemble = false, errors = m68kErrors) shouldNotBe null
|
|
m68kErrors.errors.size shouldBe 0
|
|
m68kErrors.warnings.any { it.contains("recursive subroutine") } shouldBe false
|
|
|
|
val otherErrors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(C64Target(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = false, assemble = false, errors = otherErrors) shouldNotBe null
|
|
otherErrors.errors.size shouldBe 0
|
|
otherErrors.warnings.any { it.contains("recursive subroutine") } shouldBe true
|
|
}
|
|
|
|
test("slice 3: defer in a recursive subroutine is rejected on m68k only") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
ubyte hits = 0
|
|
defer hits = hits + 1
|
|
if n == 0 {
|
|
return hits
|
|
}
|
|
return rec(n-1)
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = false, assemble = false, errors = errors) shouldBe null
|
|
errors.errors.size shouldBe 1
|
|
errors.errors[0] shouldContain "defer in a recursive subroutine"
|
|
|
|
val otherErrors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(C64Target(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = false, assemble = false, errors = otherErrors) shouldNotBe null
|
|
otherErrors.errors.size shouldBe 0
|
|
}
|
|
|
|
test("slice 3: recursive subroutine using only its own state is accepted") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
ubyte local = n + 1
|
|
if n == 0 {
|
|
return local
|
|
}
|
|
return rec(n-1) + local
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
ir.contains("VREGSLOTS=") shouldBe true
|
|
val rec = subAssembly(lines, "p8b_main.p8s_rec")
|
|
rec.any { it.startsWith("link ") } shouldBe true
|
|
rec.any { it.contains("p8_regfile") } shouldBe false
|
|
}
|
|
|
|
test("other targets are unaffected") {
|
|
val src = """
|
|
main {
|
|
sub leaf() {
|
|
ubyte a
|
|
a = 5
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src, C64Target())
|
|
lines.any { "link " in it } shouldBe false
|
|
lines.any { "(a5)" in it } shouldBe false
|
|
}
|
|
|
|
test("slice 5: defer-referenced parameter is framed with an entry-time copy-in") {
|
|
// §6.1: the positive incoming slot is the parameter storage; a parameter referenced
|
|
// from a defer handler keeps its static cell and gets an entry-time copy-in
|
|
val src = """
|
|
main {
|
|
sub parm(ubyte x) {
|
|
defer x += 1
|
|
}
|
|
sub start() {
|
|
parm(1)
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val parm = subAssembly(lines, "p8b_main.p8s_parm")
|
|
// the subroutine is framed (only the incoming slot, no locals)
|
|
parm.any { it == "link a5,#0" } shouldBe true
|
|
parm.any { it == "unlk a5" } shouldBe true
|
|
// entry-time copy-in: load the incoming slot, store into the static cell, before any clearing
|
|
parm.any { it.startsWith("move.b 8(a5),") } shouldBe true
|
|
parm.any { it.contains("p8b_main.p8s_parm.p8v_x") } shouldBe true
|
|
// the static cell is kept so the block-scope defer handler can still see it
|
|
lines.any { "p8b_main.p8s_parm.p8v_x:" in it } shouldBe true
|
|
}
|
|
|
|
test("slice 5: inline-asm-referenced parameter is framed with an entry-time copy-in") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte x) -> ubyte {
|
|
return x + 2
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
%asm {{
|
|
; this assembly reads the parameter's static cell
|
|
move.b p8b_main.p8s_rec.p8v_x,d0
|
|
nop
|
|
}}
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val rec = subAssembly(lines, "p8b_main.p8s_rec")
|
|
rec.any { it == "link a5,#0" } shouldBe true
|
|
rec.any { it.startsWith("move.b 8(a5),") } shouldBe true
|
|
rec.any { it.contains("p8b_main.p8s_rec.p8v_x") } shouldBe true
|
|
// the static cell is kept for the assembly reference; the body itself also uses the
|
|
// static cell (the entry-time copy-in made it the current value)
|
|
lines.any { "p8b_main.p8s_rec.p8v_x:" in it } shouldBe true
|
|
rec.any { it.startsWith("move.b p8b_main.p8s_rec.p8v_x,") } shouldBe true
|
|
}
|
|
|
|
test("inline assembly does not prevent framing: asm subroutine is framed by default") {
|
|
val src = """
|
|
main {
|
|
sub leaf(ubyte a) -> ubyte {
|
|
%asm {{
|
|
nop
|
|
}}
|
|
return a + 1
|
|
}
|
|
sub start() {
|
|
g = leaf(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val leaf = subAssembly(lines, "p8b_main.p8s_leaf")
|
|
leaf.any { it.startsWith("link") } shouldBe true
|
|
leaf.any { it.contains("(a5)") } shouldBe true
|
|
}
|
|
|
|
test("inline assembly subroutine with %option noframe keeps the static convention") {
|
|
val src = """
|
|
main {
|
|
sub leaf(ubyte a) -> ubyte {
|
|
%option noframe
|
|
%asm {{
|
|
nop
|
|
}}
|
|
return a + 1
|
|
}
|
|
sub start() {
|
|
g = leaf(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val leaf = subAssembly(lines, "p8b_main.p8s_leaf")
|
|
leaf.any { it.startsWith("link") } shouldBe false
|
|
leaf.any { it.contains("p8b_main.p8s_leaf.p8v_a") } shouldBe true
|
|
}
|
|
|
|
test("slice 5: static-initialized local is framed and initialized at entry") {
|
|
val src = """
|
|
main {
|
|
sub leaf() {
|
|
ubyte x = 5
|
|
x += 1
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val leaf = subAssembly(lines, "p8b_main.p8s_leaf")
|
|
leaf.any { it.startsWith("link ") } shouldBe true
|
|
leaf.any { it == "move.b #5,-1(a5)" } shouldBe true
|
|
// the initialized local no longer lives in static storage
|
|
lines.none { "p8v_x:" in it } shouldBe true
|
|
}
|
|
|
|
test("slice 5: float static-initialized local is framed") {
|
|
val src = """
|
|
%option enable_floats
|
|
main {
|
|
sub leaf() {
|
|
float f = 1.5
|
|
f += 1.0
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val leaf = subAssembly(lines, "p8b_main.p8s_leaf")
|
|
leaf.any { it.startsWith("link ") } shouldBe true
|
|
leaf.any { it.contains("fmove.s fp0,-4(a5)") } shouldBe true
|
|
lines.none { "p8v_f:" in it } shouldBe true
|
|
}
|
|
|
|
test("slice 5: float array local is framed and zero-filled in the prologue") {
|
|
val src = """
|
|
%option enable_floats
|
|
main {
|
|
sub leaf() {
|
|
float[2] arr
|
|
arr[0] = 1.5
|
|
}
|
|
sub start() {
|
|
leaf()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val leaf = subAssembly(lines, "p8b_main.p8s_leaf")
|
|
leaf.any { it == "link a5,#-8" } shouldBe true
|
|
// every float element is zero-filled in the prologue (STOREZM float)
|
|
leaf.any { it.contains("fmove.s fp0,-8(a5)") } shouldBe true
|
|
leaf.any { it.contains("fmove.s fp0,-4(a5)") } shouldBe true
|
|
lines.none { "p8v_arr:" in it } shouldBe true
|
|
}
|
|
|
|
test("slice 5: recursive subroutine with a defer-referenced parameter is rejected") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
defer n = n + 1
|
|
if n == 0 {
|
|
return 0
|
|
}
|
|
return rec(n-1)
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = true, assemble = false, errors = errors) shouldBe null
|
|
// the defer rule rejects this at the compiler level; the static cell would be
|
|
// shared between live activations of the recursive subroutine
|
|
errors.errors.any { it.contains("recursive") } shouldBe true
|
|
}
|
|
|
|
test("slice 5: recursive subroutine with an inline-asm-referenced parameter is rejected") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
return rec(1)
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
%asm {{
|
|
move.b p8b_main.p8s_rec.p8v_n,d0
|
|
nop
|
|
}}
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = true, assemble = false, errors = errors) shouldBe null
|
|
errors.errors.any { it.contains("shares data between activations") } shouldBe true
|
|
errors.errors.any { it.contains("shared with other code") } shouldBe true
|
|
}
|
|
|
|
test("slice 5: recursive subroutine with a static-initialized local now compiles") {
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> ubyte {
|
|
ubyte local = 5
|
|
if n == 0 {
|
|
return local + n
|
|
}
|
|
return rec(n-1) + local
|
|
}
|
|
sub start() {
|
|
g = rec(3)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
// the static-initialized local is now frameable, so nothing keeps static storage
|
|
ir.contains("VREGSLOTS=") shouldBe true
|
|
val rec = subAssembly(lines, "p8b_main.p8s_rec")
|
|
rec.any { it.startsWith("link ") } shouldBe true
|
|
rec.any { it.contains("p8_regfile") } shouldBe false
|
|
}
|
|
|
|
test("slice 5: vreg slots of a recursive subroutine are reused when lifetimes don't overlap") {
|
|
// two recursive call results are each stored right after their call, so their lifetimes
|
|
// don't overlap and they can share one frame slot; the frame shrinks from 16 to 10 bytes
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> uword {
|
|
if n == 0
|
|
return 1
|
|
uword a = rec(n-1)
|
|
uword b = rec(n-2)
|
|
return a + b
|
|
}
|
|
ubyte @shared g = 1
|
|
sub start() {
|
|
g = lsb(rec(10))
|
|
}
|
|
}
|
|
"""
|
|
val (_, ir) = compile(src)
|
|
val subLine = ir.lineSequence().first { it.contains("SUB NAME=\"p8b_main.p8s_rec\"") }
|
|
subLine.contains("FRAMESIZE=\"10\"") shouldBe true
|
|
val slots = subLine.substringAfter("VREGSLOTS=\"").substringBefore('"').split(',').associate {
|
|
it.substringBefore(':').toInt() to it.substringAfter(':').toInt()
|
|
}
|
|
// the two call results inside rec (the call results whose registers appear in the slot
|
|
// map; the call from start uses a register outside it) share one slot
|
|
val callResults = Regex("call p8b_main\\.p8s_rec\\([^)]*\\):r(\\d+)\\.w").findAll(ir)
|
|
.map { it.groupValues[1].toInt() }
|
|
.filter { it in slots }
|
|
.toList()
|
|
callResults.size shouldBe 2
|
|
slots.getValue(callResults[0]) shouldBe slots.getValue(callResults[1])
|
|
// reuse happened: fewer distinct slots than virtual registers
|
|
slots.values.toSet().size shouldBe 3
|
|
slots.size shouldBe 7
|
|
// the layout is deterministic across compilations
|
|
val (_, ir2) = compile(src)
|
|
val subLine2 = ir2.lineSequence().first { it.contains("SUB NAME=\"p8b_main.p8s_rec\"") }
|
|
subLine2.substringAfter("FRAMESIZE") shouldBe subLine.substringAfter("FRAMESIZE")
|
|
}
|
|
|
|
test("slice 5: a recursive subroutine with a loop keeps one slot per virtual register") {
|
|
// the loop's back edge can keep values live across it, so plain last-use intervals
|
|
// would be unsound and the safe one-slot-per-register layout is kept
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> uword {
|
|
uword total = 0
|
|
for i in 0 to 5 {
|
|
total += 1
|
|
}
|
|
if n == 0
|
|
return total
|
|
return total + rec(n-1)
|
|
}
|
|
ubyte @shared g = 1
|
|
sub start() {
|
|
g = lsb(rec(10))
|
|
}
|
|
}
|
|
"""
|
|
val (_, ir) = compile(src)
|
|
val subLine = ir.lineSequence().first { it.contains("SUB NAME=\"p8b_main.p8s_rec\"") }
|
|
subLine.contains("FRAMESIZE=\"12\"") shouldBe true
|
|
subLine.contains("VREGSLOTS=\"1:-3,2:-6,3:-7,4:-10,5:-12\"") shouldBe true
|
|
val slots = subLine.substringAfter("VREGSLOTS=\"").substringBefore('"').split(',').associate {
|
|
it.substringBefore(':').toInt() to it.substringAfter(':').toInt()
|
|
}
|
|
// every register has its own dedicated slot: all offsets are distinct
|
|
slots.values.toSet().size shouldBe slots.size
|
|
}
|
|
|
|
test("slice 5: simultaneously-live virtual registers get distinct frame slots") {
|
|
// both recursive call results are live at the addition, so they cannot share a slot
|
|
val src = """
|
|
main {
|
|
sub rec(ubyte n) -> uword {
|
|
if n < 2
|
|
return n
|
|
return rec(n-1) + rec(n-2)
|
|
}
|
|
ubyte @shared g = 1
|
|
sub start() {
|
|
g = lsb(rec(10))
|
|
}
|
|
}
|
|
"""
|
|
val (_, ir) = compile(src)
|
|
val subLine = ir.lineSequence().first { it.contains("SUB NAME=\"p8b_main.p8s_rec\"") }
|
|
val slots = subLine.substringAfter("VREGSLOTS=\"").substringBefore('"').split(',').associate {
|
|
it.substringBefore(':').toInt() to it.substringAfter(':').toInt()
|
|
}
|
|
val callResults = Regex("call p8b_main\\.p8s_rec\\([^)]*\\):r(\\d+)\\.w").findAll(ir)
|
|
.map { it.groupValues[1].toInt() }
|
|
.filter { it in slots }
|
|
.toList()
|
|
callResults.size shouldBe 2
|
|
slots.getValue(callResults[0]) shouldNotBe slots.getValue(callResults[1])
|
|
}
|
|
|
|
test("slice 5: non-recursive subroutines keep the static register file and no vreg slots") {
|
|
// only re-entrant subroutines relocate their virtual registers into the frame
|
|
val src = """
|
|
main {
|
|
ubyte gv = 2
|
|
sub inner() { gv += 1 }
|
|
sub caller() {
|
|
ubyte a
|
|
inner()
|
|
a = gv
|
|
}
|
|
sub start() {
|
|
caller()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
ir.contains("VREGSLOTS") shouldBe false
|
|
val caller = subAssembly(lines, "p8b_main.p8s_caller")
|
|
caller.any { it == "link a5,#-2" } shouldBe true
|
|
caller.any { it.contains("p8_regfile") } shouldBe true
|
|
}
|
|
test("slice 5: compiler-generated temporaries inside a subroutine are framed already") {
|
|
// the desugarer of `on ... goto` creates its own auto_heap_value variables; plain
|
|
// scalar ones are ordinary subroutine locals and are framed without any extra work
|
|
val src = """
|
|
main {
|
|
sub pick(ubyte n) -> ubyte {
|
|
on n goto(smaller, bigger)
|
|
return 0
|
|
smaller:
|
|
return 1
|
|
bigger:
|
|
return 2
|
|
}
|
|
sub start() {
|
|
g = pick(1)
|
|
}
|
|
ubyte @shared g
|
|
}
|
|
"""
|
|
val (lines, ir) = compile(src)
|
|
val pick = subAssembly(lines, "p8b_main.p8s_pick")
|
|
pick.any { it.startsWith("link ") || it.startsWith("bra") } shouldBe true
|
|
// the jump table auto-variable keeps static storage on purpose: it is read-only
|
|
// constant data, and framing it would add an initialization store on every call
|
|
ir.contains("auto_heap_value") shouldBe true
|
|
}
|
|
|
|
test("zero-clear coalescing: byte[17] prologue clears with five stores") {
|
|
val src = """
|
|
main {
|
|
sub check() {
|
|
ubyte[17] a
|
|
a[0] = a[16]
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
// slot -17, run [-17, 0): one odd head byte, then four longwords
|
|
check.any { it == "link a5,#-18" } shouldBe true
|
|
check.any { it == "clr.b -17(a5)" } shouldBe true
|
|
check.count { it.startsWith("clr.l") } shouldBe 4
|
|
check.any { it == "clr.l -16(a5)" } shouldBe true
|
|
check.any { it == "clr.l -4(a5)" } shouldBe true
|
|
check.count { it.startsWith("clr.") } shouldBe 5
|
|
}
|
|
|
|
test("zero-clear coalescing: uword[8] prologue clears with four longword stores") {
|
|
val src = """
|
|
main {
|
|
sub check() {
|
|
uword[8] b
|
|
b[0] = b[7]
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
// slot -16, run [-16, 0): four longwords, no word stores left
|
|
check.any { it == "link a5,#-16" } shouldBe true
|
|
check.count { it.startsWith("clr.l") } shouldBe 4
|
|
check.none { it.startsWith("clr.w") } shouldBe true
|
|
check.none { it.startsWith("clr.b") } shouldBe true
|
|
}
|
|
|
|
test("zero-clear coalescing: adjacent byte locals coalesce across variables") {
|
|
val src = """
|
|
main {
|
|
ubyte @shared g
|
|
sub check() {
|
|
ubyte a
|
|
ubyte b
|
|
a = 1
|
|
b = 2
|
|
g = a + b
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
// a at -1, b at -2: merged run [-2, 0) clears with a single word store
|
|
check.count { it == "clr.w -2(a5)" } shouldBe 1
|
|
check.count { it.startsWith("clr.") } shouldBe 1
|
|
}
|
|
|
|
test("zero-clear coalescing: clean locals are grouped to avoid alignment gaps") {
|
|
val src = """
|
|
main {
|
|
sub check() {
|
|
ubyte a
|
|
uword w
|
|
a = 1
|
|
w = 2
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
// w placed before a so the clean region is contiguous: a at -3, w at -2..-1
|
|
check.count { it == "clr.b -3(a5)" } shouldBe 1
|
|
check.count { it == "clr.w -2(a5)" } shouldBe 1
|
|
check.none { it.startsWith("clr.l") } shouldBe true
|
|
}
|
|
|
|
test("zero-clear coalescing: @dirty locals are not cleared") {
|
|
val src = """
|
|
main {
|
|
ubyte @shared g
|
|
sub check() {
|
|
ubyte @dirty d
|
|
ubyte c
|
|
c = g
|
|
d = g
|
|
g = c + d
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
check.count { it == "clr.b -1(a5)" } shouldBe 1
|
|
check.none { it.startsWith("clr.") && "-2(a5)" in it } shouldBe true
|
|
}
|
|
|
|
test("zero-clear coalescing: initialized locals keep their init store") {
|
|
val src = """
|
|
main {
|
|
ubyte @shared g
|
|
sub check() {
|
|
ubyte a
|
|
ubyte x = 42
|
|
a = g
|
|
x += 1
|
|
g = a + x
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
// x at -2 keeps its init store; a's [-1, 0) run merges with x's byte (zeroed
|
|
// first, then set by the init store: declaration initializers lower to a body
|
|
// store, so the slot counts as clean)
|
|
check.any { it == "move.b #42,-2(a5)" } shouldBe true
|
|
check.count { it == "clr.w -2(a5)" } shouldBe 1
|
|
check.count { it.startsWith("clr.") } shouldBe 1
|
|
check.none { it.startsWith("clr.l") } shouldBe true
|
|
}
|
|
|
|
test("zero-clear coalescing: float arrays keep per-element FPU clears") {
|
|
val src = """
|
|
%option enable_floats
|
|
main {
|
|
sub check() {
|
|
float[3] f
|
|
f[0] = 1.5
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
// three prologue clears plus the body store to f[0], all FPU, no integer clears
|
|
check.any { it == "fmove.s fp0,-12(a5)" } shouldBe true
|
|
check.any { it == "fmove.s fp0,-8(a5)" } shouldBe true
|
|
check.any { it == "fmove.s fp0,-4(a5)" } shouldBe true
|
|
check.none { it.startsWith("clr.") } shouldBe true
|
|
}
|
|
|
|
test("zero-clear coalescing: dirty neighbour splits a run") {
|
|
val src = """
|
|
main {
|
|
sub check() {
|
|
ubyte[4] c
|
|
ubyte[4] @dirty d
|
|
c[0] = 1
|
|
d[0] = 2
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
check.count { it == "clr.l -4(a5)" } shouldBe 1
|
|
check.count { it.startsWith("clr.") } shouldBe 1
|
|
}
|
|
|
|
test("zero-clear loop: ubyte[128] prologue clears with a dbra loop") {
|
|
val src = """
|
|
main {
|
|
sub check() {
|
|
ubyte[128] big
|
|
big[0] = big[127]
|
|
}
|
|
sub start() {
|
|
check()
|
|
}
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val check = subAssembly(lines, "p8b_main.p8s_check")
|
|
// 32 longwords hit the loop threshold: no straight clears remain
|
|
check.any { it == "lea -128(a5),a0" } shouldBe true
|
|
check.any { it == "moveq #31,d1" } shouldBe true
|
|
check.any { it == "move.l d0,(a0)+" } shouldBe true
|
|
check.count { it.startsWith("dbra d1,zeroloop_") } shouldBe 1
|
|
check.none { it.startsWith("clr.") } shouldBe true
|
|
}
|
|
|
|
test("stack argument pushes carry the parameter name as a comment") {
|
|
val src = """
|
|
main {
|
|
sub withp(ubyte first, word second, long third) -> long {
|
|
return second + third + first
|
|
}
|
|
sub start() {
|
|
g = lsb(withp(1, 2, 3))
|
|
}
|
|
ubyte g
|
|
}
|
|
"""
|
|
val (lines, _) = compile(src)
|
|
val start = subAssembly(lines, "p8b_main.p8s_start")
|
|
// each push store is annotated with the short parameter name it fills;
|
|
// the byte slot-reserving subq lines stay absent (byte pushes are single moves)
|
|
start.any { it.startsWith("move.b ") && it.contains(",-(sp)") && it.contains("; first") && it.contains("pushed as a word") } shouldBe true
|
|
start.any { it.startsWith("move.w ") && it.contains(",-(sp)") && it.endsWith("; second") } shouldBe true
|
|
start.any { it.startsWith("move.l ") && it.contains(",-(sp)") && it.endsWith("; third") } shouldBe true
|
|
start.count { it == "subq.l #2,sp" } shouldBe 0
|
|
}
|
|
|
|
test("re-entrant subroutines that share data are rejected in plain language") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
spawn(&task1)
|
|
}
|
|
sub spawn(pointer task) {
|
|
ubyte x = 1
|
|
x++
|
|
dispatch(task)
|
|
}
|
|
sub dispatch(pointer task) {
|
|
call(task)
|
|
}
|
|
sub task1() {
|
|
ubyte counter = 0
|
|
counter++
|
|
spawn(&task1)
|
|
}
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = true, assemble = false, errors = errors) shouldBe null
|
|
errors.errors.size shouldBe 3
|
|
val all = errors.errors.joinToString("\n")
|
|
all shouldContain "recursive (or mutually recursive)"
|
|
all shouldContain "shares data between activations"
|
|
all shouldContain "Make the data private"
|
|
all shouldContain "%option noframe"
|
|
all shouldContain "variable 'x'"
|
|
all shouldContain "variable 'counter'"
|
|
all shouldContain "temporary value"
|
|
}
|
|
|
|
test("%option noframe on a subroutine exempts it from the reentrancy static-state error") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
spawn(&task1)
|
|
}
|
|
sub spawn(pointer task) {
|
|
%option noframe
|
|
dispatch(task)
|
|
}
|
|
sub dispatch(pointer task) {
|
|
%option noframe
|
|
call(task)
|
|
}
|
|
sub task1() {
|
|
%option noframe
|
|
ubyte counter = 0
|
|
counter++
|
|
spawn(&task1)
|
|
}
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
val outputDir = tempdir().toPath()
|
|
compileText(Qemu68kTarget(), optimize = false, src, outputDir,
|
|
writeAssembly = true, assemble = false, errors = errors) shouldNotBe null
|
|
errors.errors shouldBe emptyList()
|
|
// spawn takes a parameter so it would normally get a frame; the noframe mark forces static convention
|
|
val (lines, _) = compile(src, Qemu68kTarget())
|
|
for (sub in listOf("p8b_main.p8s_spawn", "p8b_main.p8s_dispatch", "p8b_main.p8s_task1")) {
|
|
subAssembly(lines, sub).none { it.startsWith("link") } shouldBe true
|
|
}
|
|
}
|
|
|
|
test("coroutine tasks with frame-private state need no mark and get stack frames") {
|
|
val src = """
|
|
%import coroutines
|
|
|
|
main {
|
|
sub start() {
|
|
void coroutines.add(task1, 0)
|
|
void coroutines.add(task2, 0)
|
|
coroutines.run(0)
|
|
}
|
|
sub task1() {
|
|
ubyte count = 0
|
|
repeat 3 {
|
|
count += helper(count)
|
|
void coroutines.yield()
|
|
}
|
|
}
|
|
sub helper(ubyte v) -> ubyte {
|
|
return v + 1
|
|
}
|
|
sub task2() {
|
|
ubyte n = 0
|
|
repeat 2 {
|
|
n += 10
|
|
void coroutines.yield()
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
val outputDir = tempdir().toPath()
|
|
compileText(Qemu68kTarget(), optimize = false, src, outputDir,
|
|
writeAssembly = true, assemble = false, errors = errors) shouldNotBe null
|
|
errors.errors shouldBe emptyList()
|
|
// each task runs on its own private stack, so locals live in frames: no marks needed
|
|
val (lines, _) = compile(src, Qemu68kTarget())
|
|
for (sub in listOf("p8b_main.p8s_task1", "p8b_main.p8s_task2", "p8b_main.p8s_helper")) {
|
|
subAssembly(lines, sub).any { it.startsWith("link") } shouldBe true
|
|
}
|
|
}
|
|
|
|
test("%option with a non-noframe argument is rejected in a subroutine") {
|
|
val src = """
|
|
main {
|
|
sub start() {
|
|
%option force_output
|
|
}
|
|
}
|
|
"""
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(),
|
|
writeAssembly = false, assemble = false, errors = errors) shouldBe null
|
|
errors.errors.size shouldBe 1
|
|
errors.errors[0] shouldContain "not valid for subroutines"
|
|
}
|
|
})
|