mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
vertical slice prototype for m68k stack mememory model
This commit is contained in:
1 parent
822cfd4f35
commit
26bbd6c1fe
18 files changed
+617
-7
No files matched your search
@@ -0,0 +1,206 @@
|
||||
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.intermediate.*
|
||||
|
||||
/**
|
||||
* Vertical Slice Prototype (m68k-stack-memory-model.md §17): classify leaf subroutines as
|
||||
* "frameable", move their plain-data locals into A5-relative stack frame slots, rewrite all
|
||||
* references to AddressBase.FrameSlot, remove them from the static (BSS) variable list,
|
||||
* zero-clear them in the prologue and stamp the frame size on IRSubroutine.
|
||||
*
|
||||
* A subroutine is frameable when ALL of:
|
||||
* - it has no parameters,
|
||||
* - it contains no CALL/CALLI/CALLFAR/CALLFARVB/SYSCALL (leaf; also rules out self-recursion),
|
||||
* - none of its locals is address-taken (SymbolAddress immediate), referenced by inline
|
||||
* assembly or static-initialized,
|
||||
* - every local is a byte/word/long/pointer scalar or a plain numeric/bool array.
|
||||
*
|
||||
* Everything else keeps the existing static path verbatim. Deferring subroutines gain a
|
||||
* handler-invoke call during DeferProcessor flattening, so they are never frameable here;
|
||||
* call-live vregs cannot exist in a leaf, so the flat p8_regfile may stay program-static
|
||||
* in the prototype.
|
||||
*/
|
||||
class StackFrameLayout(private val program: IRProgram, private val errors: IErrorReporter) {
|
||||
|
||||
companion object {
|
||||
const val MAX_FRAME_SIZE = 16384 // §3: per-frame limit, far below the 16-bit displacement range
|
||||
}
|
||||
|
||||
private val target = program.options.compTarget
|
||||
|
||||
private class FrameVar(val name: String, val slot: Int, val elemDt: DataType, val count: Int, val dirty: Boolean)
|
||||
|
||||
fun apply() {
|
||||
val allSubs = program.allSubs().toList()
|
||||
|
||||
val referencedIn = mutableMapOf<String, MutableSet<String>>()
|
||||
val addressTaken = mutableSetOf<String>()
|
||||
val asmTexts = mutableListOf<String>()
|
||||
|
||||
fun noteRefs(chunk: IRCodeChunkBase, sub: String?) {
|
||||
chunk.instructions.forEach { instr ->
|
||||
instr.memory?.symbolName?.let { referencedIn.getOrPut(it) { mutableSetOf() } += (sub ?: "<top>") }
|
||||
(instr.immediate as? ImmediateOperand.SymbolAddress)?.let { addressTaken += it.symbol }
|
||||
}
|
||||
}
|
||||
|
||||
for (sub in allSubs) sub.forEachChunk { noteRefs(it, sub.label) }
|
||||
program.globalInits.forEachChunkRecursive { noteRefs(it, null) }
|
||||
program.blocks.forEach { block ->
|
||||
block.children.filterIsInstance<IRCodeChunkBase>().forEach { chunk ->
|
||||
chunk.forEachChunkRecursive { noteRefs(it, null) }
|
||||
}
|
||||
}
|
||||
program.allAsmSubs().forEach { asmSub -> asmTexts += asmSub.asmChunk.assembly }
|
||||
program.forEachChunk { chunk -> if (chunk is IRInlineAsmChunk) asmTexts += chunk.assembly }
|
||||
|
||||
allSubs.forEach { frameableSubroutine(it, referencedIn, addressTaken, asmTexts) }
|
||||
}
|
||||
|
||||
private fun frameableSubroutine(
|
||||
sub: IRSubroutine,
|
||||
referencedIn: Map<String, Set<String>>,
|
||||
addressTaken: Set<String>,
|
||||
asmTexts: List<String>
|
||||
) {
|
||||
if (sub.parameters.isNotEmpty()) return
|
||||
if (sub.frameSize != 0) return
|
||||
var hasInlineAsm = false
|
||||
sub.forEachChunk { chunk -> if (chunk is IRInlineAsmChunk) hasInlineAsm = true }
|
||||
if (hasInlineAsm) return
|
||||
val callOpcodes = setOf(Opcode.CALL, Opcode.CALLI, Opcode.CALLFAR, Opcode.CALLFARVB, Opcode.SYSCALL)
|
||||
if (sub.chunks.any { containsCall(it, callOpcodes) }) return
|
||||
|
||||
// candidate locals: symbols referenced only from this sub, scoped under the sub label
|
||||
val prefix = sub.label + "."
|
||||
val candidates = referencedIn.keys.filter { name ->
|
||||
name.startsWith(prefix) && referencedIn.getValue(name) == setOf(sub.label)
|
||||
}.sorted()
|
||||
if (candidates.isEmpty()) return
|
||||
|
||||
val frameVars = mutableListOf<FrameVar>()
|
||||
var cursor = 0
|
||||
for (name in candidates) {
|
||||
val v = program.st.lookup(name) as? IRStStaticVariable ?: continue
|
||||
val elemDt = isFrameableVariable(v, name, addressTaken, asmTexts) ?: continue
|
||||
val count = v.length?.toInt() ?: 1
|
||||
val size = target.memorySize(v.dt, v.length?.toInt())
|
||||
if (size <= 0 || size >= MAX_FRAME_SIZE) continue
|
||||
val alignment = when {
|
||||
size >= 4 -> 4
|
||||
size >= 2 -> 2
|
||||
else -> 1
|
||||
}
|
||||
cursor = cursor.floorDiv(alignment) * alignment // align down (cursor grows negative)
|
||||
cursor -= size
|
||||
frameVars += FrameVar(name, cursor, elemDt, count, v.dirty)
|
||||
}
|
||||
if (frameVars.isEmpty()) return
|
||||
|
||||
val frameSize = (-cursor + 1) / 2 * 2
|
||||
if (frameSize > MAX_FRAME_SIZE) {
|
||||
errors.err("frame size $frameSize of subroutine ${sub.label} exceeds the $MAX_FRAME_SIZE byte limit; " +
|
||||
"reduce local variable usage", sub.position)
|
||||
return
|
||||
}
|
||||
|
||||
// every static (base slot + displacement) access combination must stay in the 16-bit displacement range
|
||||
val slotBySymbol = frameVars.associate { it.name to it.slot }
|
||||
var outOfRange = false
|
||||
sub.forEachInstruction { instr ->
|
||||
val disp = when (val mem = instr.memory) {
|
||||
is MemoryReference.Direct -> slotBySymbol[mem.symbolName]?.plus(mem.displacement)
|
||||
is MemoryReference.Indexed -> slotBySymbol[mem.symbolName]?.plus(mem.displacement)
|
||||
else -> null
|
||||
}
|
||||
if (disp != null && (disp < -32768 || disp > 32767)) outOfRange = true
|
||||
}
|
||||
if (outOfRange) {
|
||||
errors.err("frame-relative displacement out of 16-bit range in subroutine ${sub.label}", sub.position)
|
||||
return
|
||||
}
|
||||
|
||||
// rewrite references: Symbol base -> FrameSlot base
|
||||
sub.forEachChunk { chunk ->
|
||||
val instrs = chunk.instructions
|
||||
for (i in instrs.indices) {
|
||||
val instr = instrs[i]
|
||||
val mem = instr.memory ?: continue
|
||||
val slot = slotBySymbol[mem.symbolName] ?: continue
|
||||
val newMem = when (mem) {
|
||||
is MemoryReference.Direct -> mem.copy(base = AddressBase.FrameSlot(slot))
|
||||
is MemoryReference.Indexed -> mem.copy(base = AddressBase.FrameSlot(slot))
|
||||
else -> continue
|
||||
}
|
||||
instrs[i] = instr.mapMemoryReferences { newMem }
|
||||
}
|
||||
}
|
||||
|
||||
// prologue zeroing for clean (non-dirty) frame vars; DIRTY vars stay intentionally uninitialized
|
||||
val clearInstrs = mutableListOf<IRInstruction>()
|
||||
for (fv in frameVars) {
|
||||
if (fv.dirty) continue
|
||||
val irType = irTypeFor(fv.elemDt) ?: continue
|
||||
val elemSize = target.memorySize(fv.elemDt, null)
|
||||
for (element in 0 until fv.count) {
|
||||
clearInstrs += IRInstructions.storeZero(Opcode.STOREZM, irType, IRMemory.frameDirect(fv.slot, element * elemSize))
|
||||
}
|
||||
}
|
||||
|
||||
// the variables now live per-activation; remove them from the static (BSS) symbol table
|
||||
slotBySymbol.keys.forEach { program.st.removeIfExists(it) }
|
||||
|
||||
if (clearInstrs.isNotEmpty()) {
|
||||
// prepend into the existing first chunk (it carries the sub label; the chunk list
|
||||
// structure and its label invariant must not change)
|
||||
val firstChunk = sub.chunks.first() as IRCodeChunk
|
||||
firstChunk.instructions.addAll(0, clearInstrs)
|
||||
}
|
||||
sub.frameSize = frameSize
|
||||
}
|
||||
|
||||
private fun containsCall(chunk: IRCodeChunkBase, callOpcodes: Set<Opcode>): Boolean {
|
||||
var found = false
|
||||
chunk.forEachChunkRecursive { c -> if (c.instructions.any { it.opcode in callOpcodes }) found = true }
|
||||
return found
|
||||
}
|
||||
|
||||
/** returns the element data type when the variable is frameable, null otherwise */
|
||||
private fun isFrameableVariable(v: IRStStaticVariable, name: String, addressTaken: Set<String>, asmTexts: List<String>): DataType? {
|
||||
if (name in addressTaken) return null
|
||||
if (asmTexts.any { it.contains(name) }) return null
|
||||
if (v.initializationValue != null) return null // static-initialized: must keep static storage
|
||||
if (v.align > 0u) return null // explicit alignment request: keep static
|
||||
if (v.zpwish == ZeropageWish.REQUIRE_ZEROPAGE || v.zpwish == ZeropageWish.PREFER_ZEROPAGE) return null
|
||||
return when {
|
||||
v.dt.isPointer -> v.dt // pointer storage is always just an address
|
||||
v.dt.isBasic && v.dt.isNumericOrBool -> v.dt
|
||||
v.dt.isArray && v.dt.sub != null && v.dt.sub != BaseDataType.POINTER -> {
|
||||
val subDt = baseToScalar(v.dt.sub!!) ?: return null
|
||||
if (subDt.isFloat || subDt.isStructInstance || subDt.isString || subDt.isPointer) null else subDt
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun baseToScalar(base: BaseDataType): DataType? = when (base) {
|
||||
BaseDataType.UBYTE -> DataType.UBYTE
|
||||
BaseDataType.BYTE -> DataType.BYTE
|
||||
BaseDataType.BOOL -> DataType.BOOL
|
||||
BaseDataType.UWORD -> DataType.UWORD
|
||||
BaseDataType.WORD -> DataType.WORD
|
||||
BaseDataType.LONG -> DataType.LONG
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun irTypeFor(dt: DataType): IRDataType? = when {
|
||||
dt.isByteOrBool -> IRDataType.BYTE
|
||||
dt.isWord -> IRDataType.WORD
|
||||
dt.isLong || dt.isPointer -> IRDataType.LONG
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -281,6 +281,17 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
output.appendLine(code)
|
||||
}
|
||||
|
||||
// === stack frame context (Vertical Slice Prototype: frame residents' locals) ===
|
||||
// non-zero while emitting a subroutine that has a frame; the return translation
|
||||
// emits `unlk a5` before each rts when this is set.
|
||||
internal var currentFrameSize = 0
|
||||
|
||||
/** emits the frame teardown for the subroutine currently being translated, if it has a frame */
|
||||
internal fun emitFrameUnlk() {
|
||||
if (currentFrameSize > 0)
|
||||
emitLine("unlk a5")
|
||||
}
|
||||
|
||||
// === virtual register file layout (1, 2 or 4 bytes per slot depending on type, word-aligned) ===
|
||||
private data class RegFileLayout(val offsets: Map<Int, Int>, val totalSize: Int)
|
||||
|
||||
@@ -454,6 +465,12 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
|
||||
private fun resolveMemoryBase(base: AddressBase, displacement: Int): String {
|
||||
val resolved = when (base) {
|
||||
// combine slot offset and reference displacement into a single a5-relative displacement
|
||||
is AddressBase.FrameSlot -> {
|
||||
val total = base.offset + displacement
|
||||
if (total !in -32768..32767) error("frame displacement $total out of 16-bit range")
|
||||
return "$total(a5)"
|
||||
}
|
||||
is AddressBase.Symbol -> resolveSymbolRef(base.name)
|
||||
is AddressBase.Absolute -> base.address.value.toHex()
|
||||
}
|
||||
@@ -728,6 +745,9 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
val entrypointNames = setOf("p8b_main.p8s_start", "main.start")
|
||||
if(sub.label in entrypointNames)
|
||||
emitLine("bsr run_global_inits")
|
||||
currentFrameSize = sub.frameSize
|
||||
if (sub.frameSize > 0)
|
||||
emitLine("link a5,#-${sub.frameSize}")
|
||||
val livenessInstructions = sub.chunks.filterIsInstance<IRCodeChunk>().flatMap { it.instructions }
|
||||
val deadStoreSuppressionAllowed = canSuppressDeadStores(sub)
|
||||
var instructionOffset = 0
|
||||
@@ -759,6 +779,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
emitRaw("; End of subroutine: $subLabel")
|
||||
emitRaw("")
|
||||
currentFrameSize = 0
|
||||
}
|
||||
|
||||
private fun emitAsmSubroutine(sub: IRAsmSubroutine) {
|
||||
|
||||
@@ -131,6 +131,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
|
||||
Opcode.RETURN -> {
|
||||
invalidateD0Cache()
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -144,6 +145,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(reg, type)
|
||||
}
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -156,6 +158,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
emitLine("moveq #$value, d0")
|
||||
else
|
||||
emitLine("move$s #$value, d0")
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.codegen.intermediate.IRCodeGen
|
||||
import prog8.codegen.intermediate.StackFrameLayout
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRFileWriter
|
||||
import prog8.intermediate.VirtualRegister
|
||||
@@ -28,6 +29,9 @@ class M68kCodeGenerator(val retainSSA: Boolean): ICodeGeneratorBackend {
|
||||
}
|
||||
irProgram.verifyRegisterTypes(virtualRegisterTypes)
|
||||
|
||||
// Vertical Slice Prototype: stack-frame locals for frameable leaf subroutines
|
||||
StackFrameLayout(irProgram, errors).apply()
|
||||
|
||||
if (options.dumpVariables)
|
||||
dumpVariables(irProgram)
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package prog8tests.codegen.m68k
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.core.Position
|
||||
import prog8.code.core.ZeropageType
|
||||
import prog8.code.target.Qemu68kTarget
|
||||
import prog8.codegen.m68k.AsmGen
|
||||
import prog8.intermediate.*
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
|
||||
// Vertical Slice Prototype: m68k backend lowering of FrameSlot memory references
|
||||
// and the link/unlk frame prologue/epilogue (m68k-stack-memory-model.md §17).
|
||||
class TestStackFrameEmission : FunSpec({
|
||||
|
||||
val tempRoot = tempdir().toPath()
|
||||
|
||||
fun generateAsm(frameSize: Int, instructions: List<IRInstruction>): List<String> {
|
||||
val options = CompilationOptions.builder(Qemu68kTarget())
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.floats(false)
|
||||
.compilerVersion("test")
|
||||
.memtopAddress(0xffffu)
|
||||
.optimize(true)
|
||||
.build()
|
||||
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
||||
program.options.outputDir = tempRoot
|
||||
val chunk = IRCodeChunk(null, null)
|
||||
chunk.instructions.addAll(instructions)
|
||||
val sub = IRSubroutine("test.start", emptyList(), emptyList(), Position.DUMMY, frameSize)
|
||||
sub.chunks.add(chunk)
|
||||
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
||||
block.children.add(sub)
|
||||
program.blocks.add(block)
|
||||
|
||||
tempRoot.toFile().deleteRecursively()
|
||||
tempRoot.toFile().mkdirs()
|
||||
AsmGen(program, Qemu68kTarget()).generate()
|
||||
val asmFile = tempRoot.resolve("test.asm")
|
||||
check(asmFile.exists()) { "Assembly file not written: $asmFile" }
|
||||
return asmFile.readText().lines().map { it.trim() }
|
||||
}
|
||||
|
||||
test("frameless subroutine emits no frame setup") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("link") } shouldBe false
|
||||
lines.any { it.startsWith("unlk") } shouldBe false
|
||||
}
|
||||
|
||||
test("framed subroutine emits link prologue and unlk epilogue") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it == "link a5,#-16" } shouldBe true
|
||||
lines.count { it == "unlk a5" } shouldBe 1
|
||||
lines.any { "-1(a5)" in it } shouldBe true
|
||||
// epilogue must directly precede the subroutine's return
|
||||
lines[lines.indexOf("unlk a5") + 1] shouldBe "rts"
|
||||
}
|
||||
|
||||
test("frame slot direct references lower to displacements relative to a5") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeImmediate(IRDataType.WORD, 5, IRMemory.frameDirect(-4)),
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.memoryOpImmediate(Opcode.ADDIM, IRDataType.BYTE, IRMemory.frameDirect(-1), 3),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.contains("-4(a5)") && it.startsWith("move.w") } shouldBe true
|
||||
lines.any { it.contains("-1(a5)") } shouldBe true
|
||||
lines.any { it.startsWith("addq.b") || it.startsWith("add.b") } shouldBe true
|
||||
}
|
||||
|
||||
test("indexed frame slot access uses lea of the slot base plus index") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
|
||||
IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("lea") && "-8(a5)" in it } shouldBe true
|
||||
lines.count { it.contains("(a0,d0.w)") } shouldBe 2
|
||||
}
|
||||
|
||||
test("frame displacement combines slot offset and reference displacement") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-8, 4)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
// -8 slot + 4 displacement = -4(a5), a single combined displacement
|
||||
lines.any { it.contains("-4(a5)") } shouldBe true
|
||||
lines.any { it.contains("-8(a5)+4") } shouldBe false
|
||||
}
|
||||
})
|
||||
@@ -1114,6 +1114,7 @@ internal fun AsmGen.resolveAddress(base: AddressBase, displacement: Int = 0): St
|
||||
if (displacement != 0) "$resolved+$displacement" else resolved
|
||||
}
|
||||
is AddressBase.Absolute -> base.address.value.toHex()
|
||||
is AddressBase.FrameSlot -> error("stack frame slots are only supported on m68k targets, got $base")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package prog8tests.compiler
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.Qemu68kTarget
|
||||
import prog8.intermediate.IRFileReader
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
// Vertical Slice Prototype end-to-end tests (m68k-stack-memory-model.md §17):
|
||||
// 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() ?: "")
|
||||
}
|
||||
|
||||
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)
|
||||
// locals laid out below a5: a at -1, arr at -8..-5, b at -10..-9 -> frame size 10
|
||||
lines.any { it == "link a5,#-10" } 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 { "-1(a5)" in it } shouldBe true
|
||||
lines.any { "-10(a5)" in it } shouldBe true
|
||||
lines.any { "-5(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:-1") shouldBe true
|
||||
ir.contains("FRAMESIZE=\"10\"") shouldBe true
|
||||
val reloaded = IRFileReader().read(ir)
|
||||
reloaded.allSubs().first { it.label.endsWith("p8s_leaf") }.frameSize shouldBe 10
|
||||
}
|
||||
|
||||
test("subroutine with parameters keeps the static path") {
|
||||
val src = """
|
||||
main {
|
||||
sub withparam(ubyte v) {
|
||||
ubyte a
|
||||
a = v
|
||||
}
|
||||
sub start() {
|
||||
withparam(1)
|
||||
}
|
||||
}
|
||||
"""
|
||||
val (lines, _) = compile(src)
|
||||
lines.any { it.startsWith("link ") } shouldBe false
|
||||
lines.any { "-1(a5)" in it } shouldBe false
|
||||
}
|
||||
|
||||
test("subroutine that calls another is not framed") {
|
||||
val src = """
|
||||
main {
|
||||
ubyte gv = 2
|
||||
sub inner() { gv += 1 }
|
||||
sub caller() {
|
||||
ubyte a
|
||||
inner()
|
||||
a = gv
|
||||
}
|
||||
sub start() {
|
||||
caller()
|
||||
}
|
||||
}
|
||||
"""
|
||||
val (lines, _) = compile(src)
|
||||
lines.any { it.startsWith("link ") } shouldBe false
|
||||
}
|
||||
|
||||
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("defer-referenced subroutine is not framed") {
|
||||
val src = """
|
||||
main {
|
||||
sub leaf() {
|
||||
ubyte a = 1
|
||||
defer a += 1
|
||||
}
|
||||
sub start() {
|
||||
leaf()
|
||||
}
|
||||
}
|
||||
"""
|
||||
val (lines, _) = compile(src)
|
||||
// DeferProcessor adds handler-call instructions -> the sub contains calls -> never frameable
|
||||
lines.any { it.startsWith("link ") } shouldBe false
|
||||
}
|
||||
|
||||
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("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
|
||||
}
|
||||
})
|
||||
@@ -81,7 +81,7 @@ class IRFileReader {
|
||||
val compilerVersion = start.attributes.asSequence().single { it.name.localPart == "COMPILERVERSION" }.value
|
||||
val irFormat = start.attributes.asSequence().singleOrNull { it.name.localPart == "IRFORMAT" }?.value
|
||||
?: throw IRParseException("missing IRFORMAT")
|
||||
if(irFormat != IR_FORMAT_VERSION.toString())
|
||||
if(irFormat.toIntOrNull() == null || irFormat.toInt() !in IR_FORMAT_MIN_SUPPORTED..IR_FORMAT_VERSION)
|
||||
throw IRParseException("unsupported IR format: $irFormat")
|
||||
options = parseOptions(reader, compilerVersion)
|
||||
val asmsymbols = parseAsmSymbols(reader)
|
||||
@@ -585,7 +585,8 @@ class IRFileReader {
|
||||
val sub = IRSubroutine(attrs.getValue("NAME"),
|
||||
parseParameters(reader),
|
||||
if(returns=="") emptyList() else returns.split(',').map { parseDatatype(it, false) },
|
||||
parsePosition(attrs.getValue("POS")))
|
||||
parsePosition(attrs.getValue("POS")),
|
||||
attrs["FRAMESIZE"]?.toIntOrNull() ?: 0)
|
||||
|
||||
skipText(reader)
|
||||
while(reader.peek().isStartElement) {
|
||||
|
||||
@@ -145,6 +145,8 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
|
||||
xml.writeAttribute("NAME", child.label)
|
||||
xml.writeAttribute("RETURNS", child.returns.joinToString(",") { it.irTypeString(null).lowercase() })
|
||||
xml.writeAttribute("POS", child.position.toString())
|
||||
if (child.frameSize != 0)
|
||||
xml.writeAttribute("FRAMESIZE", child.frameSize.toString())
|
||||
xml.writeCharacters("\n")
|
||||
xml.writeStartElement("PARAMS")
|
||||
xml.writeCharacters("\n")
|
||||
|
||||
@@ -8,8 +8,12 @@ package prog8.intermediate
|
||||
* The version of the .p8ir file format. Format 2 uses structured instruction operands and is
|
||||
* intentionally incompatible with the older positional format. Format 3 adds union struct definitions.
|
||||
* Format 4 replaces the ARRAY_POINTER base type with ARRAY+POINTER+pointeeSub.
|
||||
* Format 5 adds the frame:<offset> memory address base (stack frame slots) and the SUB FRAMESIZE attribute.
|
||||
*/
|
||||
const val IR_FORMAT_VERSION = 4
|
||||
const val IR_FORMAT_VERSION = 5
|
||||
|
||||
/** lowest .p8ir format version this reader accepts; version 5 only adds syntax that v4 files never use */
|
||||
const val IR_FORMAT_MIN_SUPPORTED = 4
|
||||
|
||||
object IRFormat {
|
||||
// Regex patterns for parsing IR text lines
|
||||
|
||||
@@ -21,6 +21,19 @@ object IRMemory {
|
||||
fun direct(address: UInt, displacement: Int = 0): MemoryReference =
|
||||
MemoryReference.Direct(AddressBase.Absolute(MemoryAddress(address)), displacement)
|
||||
|
||||
fun frame(offset: Int): AddressBase = AddressBase.FrameSlot(offset)
|
||||
|
||||
fun frameDirect(offset: Int, displacement: Int = 0): MemoryReference =
|
||||
MemoryReference.Direct(AddressBase.FrameSlot(offset), displacement)
|
||||
|
||||
fun frameIndexed(
|
||||
offset: Int,
|
||||
indexRegister: Int,
|
||||
indexType: IRDataType,
|
||||
scale: Int = 1,
|
||||
displacement: Int = 0
|
||||
): MemoryReference = indexed(AddressBase.FrameSlot(offset), indexRegister, indexType, scale, displacement)
|
||||
|
||||
fun indexed(
|
||||
base: AddressBase,
|
||||
indexRegister: Int,
|
||||
|
||||
@@ -112,7 +112,7 @@ val ImmediateOperand.integerValue: Int?
|
||||
data class HardwareSlotOperand(val slot: CallingConventionSlot, val type: IRDataType)
|
||||
|
||||
|
||||
/** base address of a memory reference: a symbol, or a fixed numeric address */
|
||||
/** base address of a memory reference: a symbol, a fixed numeric address, or a stack frame slot */
|
||||
sealed interface AddressBase {
|
||||
data class Symbol(val name: String) : AddressBase {
|
||||
init {
|
||||
@@ -121,6 +121,20 @@ sealed interface AddressBase {
|
||||
}
|
||||
|
||||
data class Absolute(val address: MemoryAddress) : AddressBase
|
||||
|
||||
/**
|
||||
* A location in the current subroutine's stack frame, addressed relative to the
|
||||
* frame pointer. [offset] is the signed, frame-relative byte offset assigned by
|
||||
* the frame-layout pass (negative for locals, positive for incoming parameters);
|
||||
* it is never 0. The displacement of the enclosing MemoryReference is added on
|
||||
* top of this offset (so it stays >= 0, addressing into the slot).
|
||||
*/
|
||||
data class FrameSlot(val offset: Int) : AddressBase {
|
||||
init {
|
||||
require(offset != 0) { "frame slot offset cannot be 0" }
|
||||
require(offset in -32768..32767) { "frame slot offset out of 16-bit range: $offset" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -576,7 +576,9 @@ class IRSubroutine(
|
||||
override val label: String,
|
||||
val parameters: List<IRParam>,
|
||||
val returns: List<DataType>,
|
||||
val position: Position): IIRBlockElement {
|
||||
val position: Position,
|
||||
/** size in bytes of the local area of this subroutine's stack frame; 0 means no frame */
|
||||
var frameSize: Int = 0): IIRBlockElement {
|
||||
|
||||
class IRParam(val name: String, val dt: DataType)
|
||||
|
||||
@@ -589,6 +591,7 @@ class IRSubroutine(
|
||||
// params and return value should not be str
|
||||
require(parameters.all{ it.dt.isNumericOrBool || it.dt.isPointer }) {"parameter is not a bool, number or pointer"}
|
||||
require(returns.all { it.isNumericOrBool || it.isPointer}) {"returntype is not a bool, number or pointer"}
|
||||
require(frameSize >= 0 && frameSize % 2 == 0) {"frame size must be a non-negative even number: $frameSize"}
|
||||
}
|
||||
|
||||
operator fun plusAssign(chunk: IRCodeChunkBase) {
|
||||
|
||||
@@ -102,6 +102,7 @@ object IRTextCodec {
|
||||
private fun printAddressBase(base: AddressBase): String = when (base) {
|
||||
is AddressBase.Symbol -> base.name
|
||||
is AddressBase.Absolute -> base.address.toHex()
|
||||
is AddressBase.FrameSlot -> "frame:${base.offset}"
|
||||
}
|
||||
|
||||
fun printHardwareSlot(operand: HardwareSlotOperand): String =
|
||||
@@ -396,7 +397,12 @@ object IRTextCodec {
|
||||
return reference
|
||||
}
|
||||
|
||||
val base: AddressBase = if (parts[0].first().isLetter())
|
||||
val base: AddressBase = if (parts[0].startsWith("frame:")) {
|
||||
// frame:<signed offset>; ':' is not a legal character in a symbol name, so this never collides
|
||||
val off = parts[0].removePrefix("frame:").toIntOrNull()
|
||||
?: throw IRParseException("${schema.opcode}: invalid frame slot offset in $token")
|
||||
AddressBase.FrameSlot(off)
|
||||
} else if (parts[0].first().isLetter())
|
||||
AddressBase.Symbol(parts[0])
|
||||
else
|
||||
AddressBase.Absolute(parseAddress(parts[0], "${schema.opcode}: invalid memory address"))
|
||||
|
||||
@@ -38,7 +38,7 @@ class TestIRFileInOut: FunSpec({
|
||||
val generatedFile = writer.write()
|
||||
val lines = generatedFile.readLines()
|
||||
lines[0] shouldBe "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
|
||||
lines[1] shouldBe "<PROGRAM NAME=\"unittest-irwriter\" COMPILERVERSION=\"99.99\" IRFORMAT=\"4\">"
|
||||
lines[1] shouldBe "<PROGRAM NAME=\"unittest-irwriter\" COMPILERVERSION=\"99.99\" IRFORMAT=\"5\">"
|
||||
lines.last() shouldBe "</PROGRAM>"
|
||||
generatedFile.deleteExisting()
|
||||
lines.size shouldBeGreaterThan 20
|
||||
|
||||
@@ -38,6 +38,22 @@ class TestIRTextCodec: FunSpec({
|
||||
roundtrip("addim.b #3.b,[main.var]")
|
||||
}
|
||||
|
||||
test("frame slot memory references") {
|
||||
roundtrip("loadm.w r0.w,[frame:-8]")
|
||||
roundtrip("storem.b r0.b,[frame:-1]")
|
||||
roundtrip("storezm.w [frame:-4]")
|
||||
roundtrip("storezx.b [frame:-16+r3.w*4]")
|
||||
roundtrip("loadx.l r1.l,[frame:12+r2.w]")
|
||||
roundtrip("addm.b r5.b,[frame:-8+4]")
|
||||
roundtrip("storeim.b #\$2a.b,[frame:-2]")
|
||||
roundtrip("incm.w [frame:-10]")
|
||||
IRTextCodec.print(IRTextCodec.parse("loadm.w r0.w,[frame:-8]")) shouldBe "loadm.w r0.w,[frame:-8]"
|
||||
// frame:0 is illegal (a slot offset of 0 is never valid)
|
||||
shouldThrow<Exception> { IRTextCodec.parse("loadm.w r0.w,[frame:0]") }
|
||||
// negative displacement on the reference itself stays illegal
|
||||
shouldThrow<Exception> { IRTextCodec.parse("loadm.w r0.w,[main.var+-3]") }
|
||||
}
|
||||
|
||||
test("hardware slots") {
|
||||
roundtrip("loadhr.w r1.w,s10.w")
|
||||
roundtrip("storehr.b r1.b,s0.b")
|
||||
|
||||
@@ -18,6 +18,7 @@ class VmProgramLoader {
|
||||
|
||||
fun load(irProgram: IRProgram, memory: Memory): Pair<List<IRCodeChunk>, Map<String, UInt>> {
|
||||
irProgram.validate()
|
||||
rejectFrameBasedIr(irProgram)
|
||||
irProgram.st.stripAllPrefixes()
|
||||
placeholders.clear()
|
||||
subroutines.clear()
|
||||
@@ -330,6 +331,24 @@ class VmProgramLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame-based IR (stack frame slot memory model) is only supported by the m68k
|
||||
* backend for now; the VM's activation-record support is a follow-up
|
||||
* (m68k-stack-memory-model.md §9). Reject explicitly instead of silently
|
||||
* mis-resolving frame references as static storage.
|
||||
*/
|
||||
private fun rejectFrameBasedIr(program: IRProgram) {
|
||||
program.allSubs().forEach { sub ->
|
||||
if (sub.frameSize != 0)
|
||||
throw IRParseException("frame-based IR (subroutine ${sub.label} has frame size ${sub.frameSize}) is not (yet) supported on the virtual machine")
|
||||
}
|
||||
program.forEachInstruction { instr ->
|
||||
val base = (instr.memory as? MemoryReference.Direct)?.base ?: (instr.memory as? MemoryReference.Indexed)?.base
|
||||
if (base is AddressBase.FrameSlot)
|
||||
throw IRParseException("frame-based IR (${instr.opcode} references frame slot ${base.offset}) is not (yet) supported on the virtual machine")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignArtificialLabelAddresses(program: IRProgram) {
|
||||
program.forEachChunk { chunk ->
|
||||
if(chunk !is IRCodeChunk && chunk !is IRLoopChunk)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.assertions.throwables.shouldThrowWithMessage
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldBeEmpty
|
||||
@@ -38,6 +39,33 @@ class TestVm: FunSpec( {
|
||||
vm.stepCount shouldBe 0
|
||||
}
|
||||
|
||||
test("vm rejects frame-based IR (frameSize set)") {
|
||||
val program = IRProgram("test", IRSymbolTable(), getTestOptions(), VMTarget())
|
||||
val block = IRBlock("testmain", false, IRBlock.Options(), Position.DUMMY)
|
||||
val startSub = IRSubroutine("testmain.testsub", emptyList(), emptyList(), Position.DUMMY, frameSize = 8)
|
||||
val code = IRCodeChunk(startSub.label, null)
|
||||
code += IRInstructions.returnVoid()
|
||||
startSub += code
|
||||
block += startSub
|
||||
program.addBlock(block)
|
||||
shouldThrow<IRParseException> { VirtualMachine(program) }
|
||||
.message!!.contains("not (yet) supported on the virtual machine") shouldBe true
|
||||
}
|
||||
|
||||
test("vm rejects frame-based IR (FrameSlot memory reference)") {
|
||||
val program = IRProgram("test", IRSymbolTable(), getTestOptions(), VMTarget())
|
||||
val block = IRBlock("testmain", false, IRBlock.Options(), Position.DUMMY)
|
||||
val startSub = IRSubroutine("testmain.testsub", emptyList(), emptyList(), Position.DUMMY)
|
||||
val code = IRCodeChunk(startSub.label, null)
|
||||
code += IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1))
|
||||
code += IRInstructions.returnVoid()
|
||||
startSub += code
|
||||
block += startSub
|
||||
program.addBlock(block)
|
||||
shouldThrow<IRParseException> { VirtualMachine(program) }
|
||||
.message!!.contains("not (yet) supported on the virtual machine") shouldBe true
|
||||
}
|
||||
|
||||
test("vm execution: modify memory") {
|
||||
val program = IRProgram("test", IRSymbolTable(), getTestOptions(), VMTarget())
|
||||
val block = IRBlock("testmain", false, IRBlock.Options(), Position.DUMMY)
|
||||
|
||||
Reference in new issue
Block a user