mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
bugfix for m68k stack memory model
This commit is contained in:
1 parent
bbff472457
commit
acc8705ea8
2 files changed
+245
-26
No files matched your search
@@ -1,9 +1,6 @@
|
||||
package prog8.codegen.intermediate
|
||||
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.ZeropageWish
|
||||
import prog8.code.core.*
|
||||
import prog8.intermediate.*
|
||||
|
||||
/**
|
||||
@@ -285,8 +282,9 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro
|
||||
val elemDt = isFrameableParameter(v, name, addressTaken, asmTexts) ?: return static("parameter '$name' cannot be moved into a frame")
|
||||
if (v.length != null) return static("parameter '$name' is an array")
|
||||
// a parameter referenced from outside the subroutine (defer handler, inline assembly)
|
||||
// keeps its static cell for those external references and gets an entry-time copy-in
|
||||
// from its incoming slot (§6.1); its address may not be taken (that stays a hard error)
|
||||
// keeps its static cell for ALL references (including the body's own uses) and gets
|
||||
// an entry-time copy-in from its incoming slot (§6.1); its address may not be taken
|
||||
// (that stays a hard error)
|
||||
val refs = referencedIn[name]
|
||||
if ((refs != null && refs.any { it != sub.label }) || isLabelReferencedInAsm(name, asmTexts))
|
||||
copyInParams += name
|
||||
@@ -393,13 +391,18 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro
|
||||
}
|
||||
|
||||
// rewrite references: Symbol base -> FrameSlot base (parameters additionally get the
|
||||
// right-justification displacement of their padded slot)
|
||||
// right-justification displacement of their padded slot).
|
||||
// Copy-in parameters keep their static cell for ALL references (§6.1): the entry-time
|
||||
// copy-in loads the incoming value into that cell, and the body reads and writes it
|
||||
// there - this also keeps the externally referencing code (defer handler, inline
|
||||
// assembly) observing the body's current value instead of a stale one.
|
||||
sub.forEachChunk { chunk ->
|
||||
val instrs = chunk.instructions
|
||||
for (i in instrs.indices) {
|
||||
val instr = instrs[i]
|
||||
val mem = instr.memory ?: continue
|
||||
val sym = mem.symbolName ?: continue
|
||||
if (sym in copyInParams) continue
|
||||
val slot = slotOf(sym, slotBySymbol, paramSlots) ?: continue
|
||||
val newMem = when (mem) {
|
||||
is MemoryReference.Direct ->
|
||||
@@ -672,32 +675,111 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro
|
||||
* A framed subroutine cannot branch out to another subroutine: its `link` has already adjusted
|
||||
* the stack pointer, so the other subroutine's `rts` would return into the middle of the local
|
||||
* area. Turn such a tail jump into a call followed by the normal return of this subroutine.
|
||||
* A conditional branch out (`if cc goto <sub>`) becomes an inverted branch over the tail call:
|
||||
* when the condition holds the call is executed, otherwise the skip label resumes the
|
||||
* not-taken path with the frame still live.
|
||||
*/
|
||||
private fun rewriteOutgoingJumps(framedSubLabels: Set<String>) {
|
||||
val subLabels = program.allSubs().map { it.label }.toSet()
|
||||
for (sub in program.allSubs()) {
|
||||
if (sub.label !in framedSubLabels) continue
|
||||
// collect first: rewriting below inserts chunks into the chunk lists of this sub
|
||||
val rewrites = mutableListOf<OutgoingJump>()
|
||||
sub.forEachChunk { chunk ->
|
||||
val instrs = chunk.instructions
|
||||
var i = 0
|
||||
while (i < instrs.size) {
|
||||
val instr = instrs[i]
|
||||
val target = instr.takeIf { it.opcode == Opcode.JUMP }?.target as? CodeReference.Label
|
||||
if (target != null && target.name != sub.label && target.name in subLabels) {
|
||||
instrs[i] = IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(target),
|
||||
arguments = emptyList()
|
||||
))
|
||||
instrs.add(i + 1, IRInstructions.returnVoid())
|
||||
// the RETURN is now the terminator; anything that followed the
|
||||
// original JUMP in this chunk is unreachable dead code
|
||||
while (instrs.size > i + 2)
|
||||
instrs.removeAt(instrs.size - 1)
|
||||
i++
|
||||
chunk.instructions.forEachIndexed { i, instr ->
|
||||
val target = instr.target as? CodeReference.Label ?: return@forEachIndexed
|
||||
if (target.name == sub.label || target.name !in subLabels) return@forEachIndexed
|
||||
when {
|
||||
instr.opcode == Opcode.JUMP -> rewrites += OutgoingJump(chunk, i, target)
|
||||
instr.opcode in InvertedBranchOpcodes -> rewrites += OutgoingJump(chunk, i, target)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
if (rewrites.isEmpty()) continue
|
||||
|
||||
val parentLists = mutableListOf<MutableList<IRCodeChunkBase>>()
|
||||
fun collectLists(chunks: MutableList<IRCodeChunkBase>) {
|
||||
parentLists += chunks
|
||||
chunks.filterIsInstance<IRLoopChunk>().forEach { collectLists(it.body) }
|
||||
}
|
||||
collectLists(sub.chunks)
|
||||
|
||||
for (rew in rewrites) {
|
||||
val instrs = rew.chunk.instructions
|
||||
if (instrs[rew.index].opcode == Opcode.JUMP) {
|
||||
instrs[rew.index] = IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(rew.target),
|
||||
arguments = emptyList()
|
||||
))
|
||||
instrs.add(rew.index + 1, IRInstructions.returnVoid())
|
||||
// the RETURN is now the terminator; anything that followed the
|
||||
// original JUMP in this chunk is unreachable dead code
|
||||
while (instrs.size > rew.index + 2)
|
||||
instrs.removeAt(instrs.size - 1)
|
||||
} else {
|
||||
val skipLabel = nextGeneratedLabel(sub)
|
||||
// the not-taken path continues in a fresh chunk right after this one
|
||||
val tail = IRCodeChunk(skipLabel, rew.chunk.next)
|
||||
tail.instructions.addAll(instrs.subList(rew.index + 1, instrs.size))
|
||||
val original = instrs[rew.index]
|
||||
while (instrs.size > rew.index + 1)
|
||||
instrs.removeAt(instrs.size - 1)
|
||||
instrs[rew.index] = invertedBranch(original, skipLabel)
|
||||
instrs += IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(rew.target),
|
||||
arguments = emptyList()
|
||||
))
|
||||
instrs += IRInstructions.returnVoid()
|
||||
rew.chunk.next = null // the chunk now ends with RETURN
|
||||
val parent = parentLists.first { rew.chunk in it }
|
||||
parent.add(parent.indexOf(rew.chunk) + 1, tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class OutgoingJump(val chunk: IRCodeChunkBase, val index: Int, val target: CodeReference.Label)
|
||||
|
||||
private var generatedLabelCounter = 0
|
||||
|
||||
/** unique label scoped under the subroutine, for compiler-generated control flow */
|
||||
private fun nextGeneratedLabel(sub: IRSubroutine): String =
|
||||
"${sub.label}.p8_tailskip${generatedLabelCounter++}"
|
||||
|
||||
private val InvertedStatusBranch = mapOf(
|
||||
Opcode.BSTCC to Opcode.BSTCS, Opcode.BSTCS to Opcode.BSTCC,
|
||||
Opcode.BSTEQ to Opcode.BSTNE, Opcode.BSTNE to Opcode.BSTEQ,
|
||||
Opcode.BSTNEG to Opcode.BSTPOS, Opcode.BSTPOS to Opcode.BSTNEG,
|
||||
Opcode.BSTVC to Opcode.BSTVS, Opcode.BSTVS to Opcode.BSTVC)
|
||||
|
||||
// immediate-form comparison branches keep their (register, immediate) operands
|
||||
private val InvertedImmediateBranch = mapOf(
|
||||
Opcode.BGT to Opcode.BLE, Opcode.BLE to Opcode.BGT,
|
||||
Opcode.BLT to Opcode.BGE, Opcode.BGE to Opcode.BLT,
|
||||
Opcode.BGTS to Opcode.BLES, Opcode.BLES to Opcode.BGTS,
|
||||
Opcode.BLTS to Opcode.BGES, Opcode.BGES to Opcode.BLTS)
|
||||
|
||||
// register-form comparison branches swap both operands: bgtr a,b inverts to bger b,a
|
||||
// (not(a>b) <=> b>=a); the m68k backend supports all of these opcodes directly
|
||||
private val InvertedRegisterBranch = mapOf(
|
||||
Opcode.BGTR to Opcode.BGER, Opcode.BGER to Opcode.BGTR,
|
||||
Opcode.BGTSR to Opcode.BGESR, Opcode.BGESR to Opcode.BGTSR)
|
||||
|
||||
private val InvertedBranchOpcodes: Set<Opcode> =
|
||||
InvertedStatusBranch.keys + InvertedImmediateBranch.keys + InvertedRegisterBranch.keys
|
||||
|
||||
/** the same conditional branch with the inverted condition, retargeted to [skipLabel] */
|
||||
private fun invertedBranch(instr: IRInstruction, skipLabel: String): IRInstruction {
|
||||
val skip = codeLabel(skipLabel)
|
||||
return when (val opcode = instr.opcode) {
|
||||
in InvertedStatusBranch -> instr.copy(opcode = InvertedStatusBranch.getValue(opcode), target = skip)
|
||||
in InvertedImmediateBranch -> instr.copy(opcode = InvertedImmediateBranch.getValue(opcode), target = skip)
|
||||
in InvertedRegisterBranch -> instr.copy(
|
||||
opcode = InvertedRegisterBranch.getValue(opcode),
|
||||
srcA = instr.srcB?.copy(role = OperandRole.LEFT),
|
||||
srcB = instr.srcA?.copy(role = OperandRole.RIGHT),
|
||||
target = skip)
|
||||
else -> throw AssemblyError("cannot invert conditional branch ${instr.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -335,6 +335,142 @@ main {
|
||||
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 11(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 {
|
||||
@@ -510,9 +646,10 @@ main {
|
||||
rec.any { it == "link a5,#0" } shouldBe true
|
||||
rec.any { it.startsWith("move.b 11(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 uses the frame slot
|
||||
// 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 11(a5),") && it.endsWith(",p8_regfile+0") } shouldBe true
|
||||
rec.any { it.startsWith("move.b p8b_main.p8s_rec.p8v_x,") } shouldBe true
|
||||
}
|
||||
|
||||
test("slice 5: static-initialized local is framed and initialized at entry") {
|
||||
|
||||
Reference in new issue
Block a user