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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user