vertical slice prototype for m68k stack mememory model

This commit is contained in:
Irmen de Jong committed 2026-10-07 19:06:16 +02:00
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)
+102
View File
@@ -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")
}
}
+167
View File
@@ -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"))
+1 -1
View File
@@ -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
+16
View File
@@ -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)
+28
View File
@@ -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)