mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
1134 lines
57 KiB
Kotlin
1134 lines
57 KiB
Kotlin
package prog8.codegen.intermediate
|
|
|
|
import prog8.code.core.*
|
|
import prog8.intermediate.*
|
|
|
|
/**
|
|
* Stack frame layout for m68k targets.
|
|
*
|
|
* Classifies subroutines as "frameable" and, for those:
|
|
* - moves their plain-data locals into A5-relative frame slots (negative offsets),
|
|
* - moves their parameters into the caller-pushed incoming argument area (positive offsets,
|
|
* 4 bytes per parameter, first parameter at the highest offset),
|
|
* - rewrites every memory reference to AddressBase.FrameSlot and every call site's argument
|
|
* locations to CallLocation.FrameSlot,
|
|
* - removes the moved variables from the static (BSS) symbol table,
|
|
* - emits coalesced prologue zero-clears for clean locals (parameters are written by the caller),
|
|
* - stamps frameSize / incomingSize on the IRSubroutine.
|
|
*
|
|
* Everything else keeps the existing static path verbatim. A subroutine is frameable when ALL of:
|
|
* - it contains no inline assembly chunk,
|
|
* - it makes no indirect calls and its address is never taken (subptr dispatch tables, interrupt
|
|
* handler registration, ...): an unknown caller cannot push arguments into a frame,
|
|
* - it cannot reach a subroutine whose assembly code writes to A5: such a call would overwrite
|
|
* the frame pointer while this subroutine's frame slots are still live,
|
|
* - all its parameters are frameable (a parameter that cannot move forces the whole
|
|
* subroutine to keep the static convention, since the caller must then write static cells),
|
|
* - every local it moves is plain data (byte/word/long/pointer scalar or numeric/bool array)
|
|
* that is not address-taken, inline-asm referenced, static-initialized or explicitly aligned.
|
|
* Locals that fail this test simply stay static; they do not disqualify the subroutine.
|
|
*
|
|
* A subroutine that can have two live activations (it lies on a cycle of the call graph, with
|
|
* indirect calls conservatively assumed to reach every dispatch target) must additionally move
|
|
* *everything* into its frame: its virtual registers become frame slots and each remaining
|
|
* static variable is a compile error, since two activations would share that storage.
|
|
* Such a subroutine with nothing left to share is fine on the static path.
|
|
*/
|
|
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
|
|
|
|
/** bytes reserved per incoming parameter (padded longword slot, §6.1) */
|
|
const val PARAM_SLOT_SIZE = 4
|
|
|
|
/** virtual register numbers from here on are reserved for the runtime and cannot be relocated */
|
|
const val RESERVED_VREG_RANGE_START = 99000
|
|
|
|
/** frame-pointer offset of the last-pushed (rightmost) argument slot: above saved a5 + return address */
|
|
const val INCOMING_BASE = 8
|
|
}
|
|
|
|
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 subLabels = allSubs.map { it.label }.toSet()
|
|
|
|
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 }
|
|
|
|
val callSites = collectCallSites(subLabels)
|
|
val dispatchTargets = collectDispatchTargets(subLabels, addressTaken)
|
|
val indirectCallers = allSubs.filter { sub -> containsIndirectCall(sub) }.map { it.label }.toSet()
|
|
val reentrant = subsWithMultipleLiveActivations(callSites, indirectCallers, dispatchTargets)
|
|
val framePointerClobberers = framePointerClobberingSubs()
|
|
val reachesFramePointerClobber = subsReachingFramePointerClobber(framePointerClobberers, indirectCallers)
|
|
|
|
val framedSubs = mutableMapOf<String, FrameLayoutResult>()
|
|
for (sub in allSubs) {
|
|
val outcome = frameableSubroutine(sub, referencedIn, addressTaken, asmTexts, callSites, dispatchTargets, indirectCallers, reentrant, reachesFramePointerClobber)
|
|
if (outcome is Frameability.Frameable)
|
|
framedSubs[sub.label] = outcome.layout
|
|
}
|
|
|
|
if (framedSubs.isNotEmpty())
|
|
rewriteCallArgumentLocations(framedSubs)
|
|
|
|
reportUnsoundStaticState(framedSubs, reentrant, referencedIn, addressTaken, asmTexts)
|
|
rewriteOutgoingJumps(framedSubs.keys)
|
|
}
|
|
|
|
private class CallSiteRef(val caller: String, val site: CallSite)
|
|
|
|
/** all direct calls between normal subroutines, grouped by callee label */
|
|
private fun collectCallSites(subLabels: Set<String>): Map<String, List<CallSiteRef>> {
|
|
val result = mutableMapOf<String, MutableList<CallSiteRef>>()
|
|
for (sub in program.allSubs()) {
|
|
sub.forEachChunk { chunk ->
|
|
val instrs = chunk.instructions
|
|
for (i in instrs.indices) {
|
|
val site = instrs[i].callSite ?: continue
|
|
val ref = site.codeReference
|
|
if (ref is CodeReference.Label && ref.name in subLabels) {
|
|
result.getOrPut(ref.name) { mutableListOf() } += CallSiteRef(sub.label, site)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Subroutine labels whose address is taken somewhere in the program: dispatch tables
|
|
* (static variable initializers holding symbolic references), address-taken immediates,
|
|
* or referenced from inline assembly. Such a subroutine can be entered indirectly and
|
|
* therefore possibly re-entered while an activation is live.
|
|
*/
|
|
private fun collectDispatchTargets(subLabels: Set<String>, addressTaken: Set<String>): Set<String> {
|
|
val targets = mutableSetOf<String>()
|
|
addressTaken.forEach { sym -> if (sym in subLabels) targets += sym }
|
|
program.st.allVariables().forEach { v ->
|
|
val init = v.initializationValue ?: return@forEach
|
|
val refs = when (init) {
|
|
is IRVariableInitializer.Array -> init.elements.mapNotNull { (it as? IRStSymbolicReference.Symbol)?.name }
|
|
is IRVariableInitializer.Str, is IRVariableInitializer.Numeric -> emptyList()
|
|
}
|
|
refs.forEach { name -> if (name in subLabels) targets += name }
|
|
}
|
|
return targets
|
|
}
|
|
|
|
private fun containsIndirectCall(sub: IRSubroutine): Boolean {
|
|
var found = false
|
|
sub.forEachInstruction { instr ->
|
|
val site = instr.callSite ?: return@forEachInstruction
|
|
if (site.codeReference is CodeReference.Indirect)
|
|
found = true
|
|
}
|
|
return found
|
|
}
|
|
|
|
/**
|
|
* Labels of subroutines whose code writes to A5, the frame pointer register of a framed
|
|
* subroutine. Assembly subroutines are matched on their assembly text, ordinary subroutines on
|
|
* the text of their inline assembly chunks (a subroutine without assembly cannot write A5).
|
|
*
|
|
* Such a subroutine invalidates the frame of its caller: every frame-slot reference in the
|
|
* caller then uses a different base register, and the caller's `unlk a5` restores a frame
|
|
* pointer that was never saved.
|
|
*/
|
|
private fun framePointerClobberingSubs(): Set<String> {
|
|
val result = mutableSetOf<String>()
|
|
program.allAsmSubs().forEach { asmSub ->
|
|
if (writesFramePointerRegister(asmSub.asmChunk.assembly))
|
|
result += asmSub.label
|
|
}
|
|
for (sub in program.allSubs()) {
|
|
sub.forEachChunk { chunk ->
|
|
if (chunk is IRInlineAsmChunk && writesFramePointerRegister(chunk.assembly))
|
|
result += sub.label
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
/** a frame-pointer-clobbering subroutine reached from another subroutine, and how it was reached */
|
|
private class ClobberReach(val clobberer: String, val viaIndirect: Boolean)
|
|
|
|
/**
|
|
* Subroutine label -> the frame-pointer-clobbering subroutine it can reach, for every
|
|
* subroutine that can reach one. Only real call edges are followed; a subroutine is only
|
|
* credited with reaching a clobberer through an indirect call (viaIndirect=true) when it
|
|
* can reach a subroutine that makes one, because such a call may dispatch to anything.
|
|
*/
|
|
private fun subsReachingFramePointerClobber(clobberers: Set<String>, indirectCallers: Set<String>): Map<String, ClobberReach> {
|
|
if (clobberers.isEmpty())
|
|
return emptyMap()
|
|
// Calls into %option noframe subroutines are a barrier for both propagations below:
|
|
// the mark asserts a save/restore discipline that leaves the caller's A5 intact when
|
|
// the call returns (or abandons the caller, as when switching away), so clobber reach
|
|
// through such a call is a false positive. The marked subroutines themselves stay
|
|
// classified by their own assembly.
|
|
val noframeSubs = program.allSubs().filter { it.noframe }.map { it.label }.toSet()
|
|
val callTargets = mutableMapOf<String, MutableSet<String>>()
|
|
for (sub in program.allSubs()) {
|
|
val targets = callTargets.getOrPut(sub.label) { mutableSetOf() }
|
|
sub.forEachChunk { chunk ->
|
|
chunk.instructions.forEach { instr ->
|
|
when (val ref = instr.callSite?.codeReference) {
|
|
is CodeReference.Label -> targets += ref.name
|
|
else -> {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
val reaching = mutableMapOf<String, ClobberReach>()
|
|
var changed = true
|
|
while (changed) {
|
|
changed = false
|
|
for ((label, targets) in callTargets) {
|
|
if (label in reaching)
|
|
continue
|
|
val hit = targets.filter { it !in noframeSubs }.firstOrNull { it in clobberers || it in reaching } ?: continue
|
|
reaching[label] = if (hit in clobberers) ClobberReach(hit, false) else reaching.getValue(hit)
|
|
changed = true
|
|
}
|
|
}
|
|
// no real call chain reaches a clobberer: if an indirect call is reachable (here or
|
|
// downstream), it may dispatch to any address-taken subroutine including a clobberer
|
|
val reachesIndirect = mutableSetOf<String>()
|
|
changed = true
|
|
while (changed) {
|
|
changed = false
|
|
for ((label, targets) in callTargets) {
|
|
if (label in reachesIndirect)
|
|
continue
|
|
if (label in indirectCallers || targets.filter { it !in noframeSubs }.any { it in reachesIndirect }) {
|
|
reachesIndirect += label
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
for (label in callTargets.keys) {
|
|
if (label !in reaching && label in reachesIndirect)
|
|
reaching[label] = ClobberReach(clobberers.first(), true)
|
|
}
|
|
return reaching
|
|
}
|
|
|
|
/**
|
|
* True when the assembly text writes to A5 without protecting the caller's frame pointer, in a
|
|
* form this textual check recognises: as the destination operand of an instruction, as the frame
|
|
* register of a `link`, or anywhere in a `movem` register list (whose mask syntax is not parsed
|
|
* here). The usual `move.l a5,-(sp)` ... `move.l (sp)+,a5` prologue/epilogue pair is recognised
|
|
* as balanced and does not count; a write in between does.
|
|
*
|
|
* The scan is linear and ignores branches, so a subroutine that saves A5 on one path and
|
|
* overwrites it on another is reported conservatively as a clobber. Being over-cautious only
|
|
* costs the calling subroutine its stack frame (it keeps the static convention instead).
|
|
*/
|
|
private fun writesFramePointerRegister(asm: String): Boolean {
|
|
var saved = false
|
|
for (rawLine in asm.lines()) {
|
|
// drop comments and a leading label, so a labelled instruction is still examined
|
|
val line = rawLine.substringBefore(';')
|
|
.replace(Regex("^[.0-9A-Za-z_$]+:"), "")
|
|
.trim().replace(Regex("[ \t]+"), " ")
|
|
if (line.isEmpty() || line.startsWith('.')) // equates and directives
|
|
continue
|
|
val words = line.split(' ')
|
|
val mnemonic = words[0].lowercase()
|
|
val operands = words.drop(1).joinToString(",").split(',')
|
|
if (operands.any { it.isEmpty() })
|
|
continue
|
|
if (mnemonic.startsWith("movem")) {
|
|
val pushes = operands.last().lowercase() == "-(sp)"
|
|
val mentionsA5 = operands.any { it.lowercase().contains("a5") }
|
|
if (pushes && mentionsA5) {
|
|
saved = true
|
|
continue
|
|
}
|
|
if (!pushes && operands.first().lowercase() == "(sp)+" && mentionsA5) {
|
|
saved = false
|
|
continue
|
|
}
|
|
if (mentionsA5 && !saved)
|
|
return true
|
|
continue
|
|
}
|
|
if (mnemonic == "link" && operands[0].lowercase().removePrefix("-") == "a5")
|
|
return true
|
|
val source = operands.first().lowercase()
|
|
val destination = operands.last().lowercase()
|
|
val writesA5 = destination == "a5" || destination == "-a5"
|
|
if (destination == "-(sp)" && source == "a5") {
|
|
saved = true
|
|
continue
|
|
}
|
|
if (source == "(sp)+" && writesA5 && operands.size == 2) {
|
|
saved = false
|
|
continue
|
|
}
|
|
if (writesA5 && !saved)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Labels of subroutines that can have two live activations at the same time: they lie on a
|
|
* cycle of direct calls (direct recursion or mutual recursion), or on a cycle that only closes
|
|
* through an indirect call. Uses an iterative Tarjan SCC pass over the call graph.
|
|
*/
|
|
private fun subsWithMultipleLiveActivations(
|
|
callSites: Map<String, List<CallSiteRef>>,
|
|
indirectCallers: Set<String>,
|
|
dispatchTargets: Set<String>
|
|
): Set<String> {
|
|
val edges = mutableMapOf<String, MutableSet<String>>()
|
|
for (sub in program.allSubs())
|
|
edges.getOrPut(sub.label) { mutableSetOf() }
|
|
for ((callee, refs) in callSites) {
|
|
for (ref in refs) {
|
|
val outs = edges.getOrPut(ref.caller) { mutableSetOf() }
|
|
if (outs.add(callee))
|
|
edges.getOrPut(callee) { mutableSetOf() }
|
|
}
|
|
}
|
|
// an indirect call could dispatch to any subroutine whose address is taken
|
|
for (caller in indirectCallers) {
|
|
val outs = edges.getOrPut(caller) { mutableSetOf() }
|
|
for (target in dispatchTargets)
|
|
outs.add(target)
|
|
}
|
|
|
|
val index = mutableMapOf<String, Int>()
|
|
val lowLink = mutableMapOf<String, Int>()
|
|
val onStack = mutableSetOf<String>()
|
|
val sccStack = ArrayDeque<String>()
|
|
var counter = 0
|
|
val reentrant = mutableSetOf<String>()
|
|
|
|
for (root in edges.keys) {
|
|
if (root in index) continue
|
|
index[root] = counter
|
|
lowLink[root] = counter
|
|
counter++
|
|
sccStack.addLast(root)
|
|
onStack += root
|
|
val work = ArrayDeque<Pair<String, Iterator<String>>>()
|
|
work.addLast(root to edges.getValue(root).iterator())
|
|
while (work.isNotEmpty()) {
|
|
val (node, children) = work.last()
|
|
if (children.hasNext()) {
|
|
val next = children.next()
|
|
when {
|
|
next !in index -> {
|
|
index[next] = counter
|
|
lowLink[next] = counter
|
|
counter++
|
|
sccStack.addLast(next)
|
|
onStack += next
|
|
work.addLast(next to edges.getValue(next).iterator())
|
|
}
|
|
next in onStack -> {
|
|
lowLink[node] = minOf(lowLink.getValue(node), index.getValue(next))
|
|
}
|
|
}
|
|
} else {
|
|
work.removeLast()
|
|
if (lowLink.getValue(node) == index.getValue(node)) {
|
|
val members = mutableListOf<String>()
|
|
while (true) {
|
|
val w = sccStack.removeLast()
|
|
onStack -= w
|
|
members += w
|
|
if (w == node) break
|
|
}
|
|
val cyclic = members.size > 1 || node in edges.getValue(node)
|
|
if (cyclic) reentrant += members
|
|
}
|
|
if (work.isNotEmpty()) {
|
|
val parent = work.last().first
|
|
lowLink[parent] = minOf(lowLink.getValue(parent), lowLink.getValue(node))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return reentrant
|
|
}
|
|
|
|
private class FrameLayoutResult(
|
|
//. parameter variable name -> frame offset of its incoming slot
|
|
val paramSlots: Map<String, Int>,
|
|
// parameter variable names that keep a static cell and get an entry-time copy-in (§6.1)
|
|
val copyInParams: Set<String>
|
|
)
|
|
|
|
/** outcome of the frameability decision for one subroutine */
|
|
private sealed interface Frameability {
|
|
class Frameable(val layout: FrameLayoutResult) : Frameability
|
|
class NotFrameable() : Frameability
|
|
}
|
|
|
|
private fun frameableSubroutine(
|
|
sub: IRSubroutine,
|
|
referencedIn: Map<String, Set<String>>,
|
|
addressTaken: Set<String>,
|
|
asmTexts: List<String>,
|
|
callSites: Map<String, List<CallSiteRef>>,
|
|
dispatchTargets: Set<String>,
|
|
indirectCallers: Set<String>,
|
|
reentrant: Set<String>,
|
|
reachesFramePointerClobber: Map<String, ClobberReach>
|
|
): Frameability {
|
|
val static = { Frameability.NotFrameable() }
|
|
// a %option noframe subroutine requires the legacy static convention (it manipulates
|
|
// the machine stack itself), so it never gets a frame
|
|
if (sub.noframe) return static()
|
|
if (sub.hasFrame) return static()
|
|
var hasInlineAsm = false
|
|
sub.forEachChunk { chunk -> if (chunk is IRInlineAsmChunk) hasInlineAsm = true }
|
|
if (hasInlineAsm) return static()
|
|
if (sub.label in indirectCallers) return static()
|
|
// An address-taken subroutine with parameters must stay static: an indirect call site
|
|
// cannot know which convention to pass arguments in. Without parameters there is nothing
|
|
// to pass (indirect calls never carry arguments), so framing is transparent to every
|
|
// entry path and only the re-entrancy check below still applies.
|
|
if (sub.label in dispatchTargets && sub.parameters.isNotEmpty()) return static()
|
|
if (isLabelReferencedInAsm(sub.label, asmTexts)) return static()
|
|
if (sub.label in reachesFramePointerClobber) return static()
|
|
|
|
// every parameter must be able to move: otherwise the caller keeps writing static cells
|
|
// IRParam.name is the scoped variable name; call sites refer to the same parameter by its
|
|
// last name segment only, so accept both forms.
|
|
val scopedParamNames = sub.parameters.map { param ->
|
|
if (param.name.startsWith("${sub.label}.")) param.name else "${sub.label}.${param.name}"
|
|
}
|
|
val paramSlots = mutableMapOf<String, Int>()
|
|
val paramDisplacements = mutableMapOf<String, Int>()
|
|
val copyInParams = mutableSetOf<String>()
|
|
for ((index, _) in sub.parameters.withIndex()) {
|
|
val name = scopedParamNames[index]
|
|
val v = program.st.lookup(name) as? IRStStaticVariable ?: return static()
|
|
val elemDt = isFrameableParameter(v, name, addressTaken, asmTexts) ?: return static()
|
|
if (v.length != null) return static()
|
|
// a parameter referenced from outside the subroutine (defer handler, inline assembly)
|
|
// 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
|
|
// first parameter ends up at the highest offset, last one just above the return address
|
|
val offset = INCOMING_BASE + PARAM_SLOT_SIZE * (sub.parameters.size - 1 - index)
|
|
paramSlots[name] = offset
|
|
// narrow values are right-justified in their longword slot
|
|
paramDisplacements[name] = when (irTypeFor(elemDt)) {
|
|
IRDataType.BYTE -> 3
|
|
IRDataType.WORD -> 2
|
|
else -> 0
|
|
}
|
|
}
|
|
|
|
// all call sites must use the plain parameter-memory convention of this convention
|
|
val sites = callSites[sub.label].orEmpty()
|
|
if (paramSlots.isNotEmpty()) {
|
|
if (sites.isEmpty()) return static()
|
|
for (siteRef in sites) {
|
|
val args = siteRef.site.arguments
|
|
if (args.size != sub.parameters.size) return static()
|
|
for ((argIndex, arg) in args.withIndex()) {
|
|
val loc = arg.location
|
|
if (loc !is CallLocation.ParameterMemory) return static()
|
|
if (loc.address != null) return static()
|
|
val scoped = scopedParamNames[argIndex]
|
|
if (loc.name.isNotBlank() && loc.name != scoped && loc.name != scoped.substringAfterLast('.'))
|
|
return static()
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) && name !in paramSlots
|
|
}.sorted()
|
|
// a re-entrant subroutine can still qualify with nothing but its virtual registers
|
|
if (candidates.isEmpty() && paramSlots.isEmpty() && sub.label !in reentrant)
|
|
return static()
|
|
|
|
// Collect layout info first, then place clean (zero-clear) locals together so they
|
|
// form one contiguous region that emitZeroRegions and the m68k backend can clear
|
|
// efficiently. Non-clean locals (dirty, initialized, float) are placed afterwards.
|
|
data class LocalLayout(
|
|
val name: String,
|
|
val elemDt: DataType,
|
|
val count: Int,
|
|
val size: Int,
|
|
val alignment: Int,
|
|
val dirty: Boolean,
|
|
val initialized: Boolean,
|
|
val isFloat: Boolean
|
|
)
|
|
|
|
val locals = mutableListOf<LocalLayout>()
|
|
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
|
|
}
|
|
val initialized = v.initializationValue != null
|
|
val isFloat = irTypeFor(elemDt) == IRDataType.FLOAT
|
|
locals += LocalLayout(name, elemDt, count, size, alignment, v.dirty, initialized, isFloat)
|
|
if (v.length != null && size > 8 && !v.dirty && v.initializationValue == null) {
|
|
val shortName = name.substringAfterLast('.').removePrefix("p8v_")
|
|
errors.warn("local array '$shortName' ($size bytes) in subroutine '${userName(sub)}' is zero-initialized on every call which can be costly; consider @dirty if you assign it before use", sub.position)
|
|
}
|
|
}
|
|
|
|
val sortedLocals = locals.sortedWith(
|
|
compareBy<LocalLayout> { when {
|
|
it.dirty || it.initialized || it.isFloat -> 1
|
|
else -> 0
|
|
} }
|
|
.thenByDescending { it.alignment }
|
|
.thenBy { if (it.size % it.alignment == 0) 0 else 1 } // size-aligned locals leave no gap
|
|
.thenByDescending { it.size }
|
|
.thenBy { it.name }
|
|
)
|
|
|
|
val frameVars = mutableListOf<FrameVar>()
|
|
var cursor = 0
|
|
for (local in sortedLocals) {
|
|
cursor = cursor.floorDiv(local.alignment) * local.alignment
|
|
cursor -= local.size
|
|
frameVars += FrameVar(local.name, cursor, local.elemDt, local.count, local.dirty)
|
|
}
|
|
// a subroutine that can be re-entered needs its virtual registers per-activation as well:
|
|
// the flat program-static register file is shared by all activations of the same subroutine
|
|
val vregSlots = mutableMapOf<Int, Int>()
|
|
if (sub.label in reentrant) {
|
|
val vregs = vregsUsedIn(sub)
|
|
if (vregs.keys.any { it >= RESERVED_VREG_RANGE_START })
|
|
return static()
|
|
val (newCursor, slots) = allocateVregSlots(sub, vregs, cursor)
|
|
cursor = newCursor
|
|
vregSlots.putAll(slots)
|
|
}
|
|
if (frameVars.isEmpty() && paramSlots.isEmpty() && vregSlots.isEmpty())
|
|
return static()
|
|
|
|
val frameSize = (-cursor + 1) / 2 * 2
|
|
if (frameSize > MAX_FRAME_SIZE) {
|
|
errors.err("frame size $frameSize of subroutine ${userName(sub)} exceeds the $MAX_FRAME_SIZE byte limit; " +
|
|
"reduce local variable usage", sub.position)
|
|
return static()
|
|
}
|
|
|
|
// 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 mem = instr.memory
|
|
val sym = mem?.symbolName
|
|
val slot = if (sym != null) slotOf(sym, slotBySymbol, paramSlots) else null
|
|
// parameters are right-justified in their padded longword slot, so include that displacement
|
|
val extra = paramDisplacements[sym] ?: 0
|
|
val disp = when (mem) {
|
|
is MemoryReference.Direct -> slot?.plus(mem.displacement)?.plus(extra)
|
|
is MemoryReference.Indexed -> slot?.plus(mem.displacement)?.plus(extra)
|
|
else -> null
|
|
}
|
|
if (disp != null && (disp < -32768 || disp > 32767)) outOfRange = true
|
|
// relocated virtual registers must stay in 16-bit displacement range as well
|
|
for (access in instr.registerAccesses) {
|
|
val regOffset = vregSlots[access.register.num]
|
|
if (regOffset != null && (regOffset < -32768 || regOffset > 32767)) outOfRange = true
|
|
}
|
|
}
|
|
if (outOfRange) {
|
|
errors.err("frame-relative displacement out of 16-bit range in subroutine ${userName(sub)}", sub.position)
|
|
return static()
|
|
}
|
|
|
|
// rewrite references: Symbol base -> FrameSlot base (parameters additionally get the
|
|
// 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 ->
|
|
mem.copy(base = AddressBase.FrameSlot(slot), displacement = mem.displacement + (paramDisplacements[sym] ?: 0))
|
|
is MemoryReference.Indexed ->
|
|
mem.copy(base = AddressBase.FrameSlot(slot), displacement = mem.displacement + (paramDisplacements[sym] ?: 0))
|
|
else -> continue
|
|
}
|
|
instrs[i] = instr.mapMemoryReferences { newMem }
|
|
}
|
|
}
|
|
|
|
// prologue: entry-time copy-in for externally referenced parameters (§6.1), then the
|
|
// numeric initializers of static-initialized locals, then zeroing for clean (non-dirty)
|
|
// locals; incoming parameters are written by the caller
|
|
val prologueInstrs = mutableListOf<IRInstruction>()
|
|
if (copyInParams.isNotEmpty()) {
|
|
val used = program.registersUsed()
|
|
var nextIntScratch = (used.intRegsTypes.keys.maxOfOrNull { it.value } ?: 0) + 1
|
|
var nextFloatScratch = ((used.floatRegsRead.keys + used.floatRegsWritten.keys).maxOfOrNull { it.value } ?: 0) + 1
|
|
require(nextIntScratch < RESERVED_VREG_RANGE_START) { "copy-in integer scratch register would collide with reserved vreg range" }
|
|
require(nextFloatScratch < RESERVED_VREG_RANGE_START) { "copy-in float scratch register would collide with reserved vreg range" }
|
|
for (name in copyInParams.sorted()) {
|
|
val v = program.st.lookup(name) as? IRStStaticVariable ?: continue
|
|
val elemDt = if (v.dt.isArray) baseToScalar(v.dt.sub!!) else v.dt
|
|
val irType = elemDt?.let { irTypeFor(it) } ?: continue
|
|
val offset = paramSlots[name] ?: continue
|
|
val displacement = paramDisplacements[name] ?: 0
|
|
val scratch = if (irType == IRDataType.FLOAT) nextFloatScratch++ else nextIntScratch++
|
|
prologueInstrs += IRInstructions.loadMemory(Opcode.LOADM, irType, scratch, IRMemory.frameDirect(offset, displacement))
|
|
prologueInstrs += IRInstructions.storeMemory(Opcode.STOREM, irType, scratch, IRMemory.direct(name))
|
|
}
|
|
}
|
|
// Zero regions: byte ranges [start, end) of clean (non-dirty, non-initialized)
|
|
// non-float locals. Regions are merged across variables wherever exactly
|
|
// contiguous, then emitted as aligned stores below.
|
|
val zeroRegions = mutableListOf<Pair<Int, Int>>()
|
|
for (fv in frameVars) {
|
|
val init = (program.st.lookup(fv.name) as? IRStStaticVariable)?.initializationValue as? IRVariableInitializer.Numeric
|
|
if (init != null) {
|
|
// a static-initialized local gets its initializer stored into the frame slot;
|
|
// this replaces the zero-clear for it
|
|
val irType = irTypeFor(fv.elemDt) ?: continue
|
|
if (irType == IRDataType.FLOAT)
|
|
prologueInstrs += IRInstructions.storeImmediateFloat(init.value, IRMemory.frameDirect(fv.slot))
|
|
else
|
|
prologueInstrs += IRInstructions.storeImmediate(irType, init.value.toInt(), IRMemory.frameDirect(fv.slot))
|
|
continue
|
|
}
|
|
if (fv.dirty) continue
|
|
val irType = irTypeFor(fv.elemDt) ?: continue
|
|
val elemSize = target.memorySize(fv.elemDt, null)
|
|
if (irType == IRDataType.FLOAT) {
|
|
// float scalars and float array elements are zero-filled with STOREZM float stores
|
|
// (the m68k backend supports them); the VM does not run this pass.
|
|
// Floats never join zero regions, so they also break adjacency between them.
|
|
for (element in 0 until fv.count) {
|
|
prologueInstrs += IRInstructions.storeZero(Opcode.STOREZM, irType, IRMemory.frameDirect(fv.slot, element * elemSize))
|
|
}
|
|
continue
|
|
}
|
|
zeroRegions += fv.slot to fv.slot + fv.count * elemSize
|
|
}
|
|
emitZeroRegions(prologueInstrs, zeroRegions)
|
|
|
|
// the moved variables now live per-activation; remove them from the static (BSS) symbol table.
|
|
// copy-in parameters keep their static cell for the external reference (§6.1)
|
|
(slotBySymbol.keys + paramSlots.keys.filter { it !in copyInParams }).forEach { program.st.removeIfExists(it) }
|
|
|
|
if (prologueInstrs.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, prologueInstrs)
|
|
}
|
|
sub.frameSize = frameSize
|
|
sub.incomingSize = paramSlots.size * PARAM_SLOT_SIZE
|
|
sub.frameVregSlots = vregSlots
|
|
return Frameability.Frameable(FrameLayoutResult(paramSlots, copyInParams))
|
|
}
|
|
|
|
/**
|
|
* Emits the merged zero-clear stores for [regions]. Exactly-contiguous ranges merge
|
|
* regardless of owning variable; the frameVars walk above is name-ordered rather than
|
|
* address-ordered, so regions are sorted first. Each merged run becomes one contiguous,
|
|
* address-ascending STOREZM block: the m68k backend's clear-loop detection relies on
|
|
* that shape, so nothing may be interleaved inside a run.
|
|
*/
|
|
private fun emitZeroRegions(prologueInstrs: MutableList<IRInstruction>, regions: List<Pair<Int, Int>>) {
|
|
val merged = mutableListOf<Pair<Int, Int>>()
|
|
for ((start, end) in regions.sortedBy { it.first }) {
|
|
val last = merged.lastOrNull()
|
|
if (last != null && last.second == start)
|
|
merged[merged.lastIndex] = last.first to end
|
|
else
|
|
merged += start to end
|
|
}
|
|
for ((lo, hi) in merged) {
|
|
var offset = lo
|
|
// a5-relative word/longword access requires an even address; a run starting
|
|
// odd clears its first byte separately, which restores evenness
|
|
if (offset % 2 != 0) {
|
|
prologueInstrs += IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(offset))
|
|
offset++
|
|
}
|
|
while (hi - offset >= 4) {
|
|
prologueInstrs += IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(offset))
|
|
offset += 4
|
|
}
|
|
if (hi - offset >= 2) {
|
|
prologueInstrs += IRInstructions.storeZero(Opcode.STOREZM, IRDataType.WORD, IRMemory.frameDirect(offset))
|
|
offset += 2
|
|
}
|
|
if (hi - offset == 1)
|
|
prologueInstrs += IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(offset))
|
|
}
|
|
}
|
|
|
|
/** the virtual registers used by the instructions of a subroutine, by register number */
|
|
private fun vregsUsedIn(sub: IRSubroutine): Map<Int, IRDataType> {
|
|
val result = mutableMapOf<Int, IRDataType>()
|
|
sub.forEachChunk { chunk ->
|
|
// a loop chunk aggregates its own body, which forEachChunk visits separately anyway
|
|
if (chunk is IRLoopChunk) return@forEachChunk
|
|
val used = chunk.usedRegisters()
|
|
for ((reg, type) in used.regsTypes)
|
|
result[reg.num] = type
|
|
// float registers are not typed in regsTypes; all of them are floats
|
|
for (reg in used.readRegs.keys + used.writeRegs.keys)
|
|
result.getOrPut(reg.num) { IRDataType.FLOAT }
|
|
}
|
|
return result
|
|
}
|
|
|
|
private class VregSlot(val offset: Int, val size: Int, var occupant: Int)
|
|
|
|
/**
|
|
* Frame slots for the virtual registers of a re-entrant subroutine. Unless the subroutine
|
|
* has a backward branch, register lifetimes are intervals over the flattened instruction
|
|
* order and a slot whose previous occupant is already dead is reused by a later register.
|
|
* A back edge (any loop chunk, or a branch/jump to a label at or before its own position,
|
|
* including a jump into another subroutine) can keep a value live across the jump, which
|
|
* plain last-use intervals would get wrong, so such a subroutine keeps one dedicated slot
|
|
* per register.
|
|
*/
|
|
private fun allocateVregSlots(sub: IRSubroutine, vregs: Map<Int, IRDataType>, cursor: Int): Pair<Int, Map<Int, Int>> {
|
|
if (vregs.isEmpty())
|
|
return cursor to emptyMap()
|
|
|
|
// flatten the non-loop chunks into a linear instruction list; loop chunks force the
|
|
// conservative one-slot-per-register fallback and are skipped here
|
|
val instrList = mutableListOf<IRInstruction>()
|
|
val chunkFirstIndex = mutableMapOf<String, Int>()
|
|
var containsLoop = false
|
|
sub.forEachChunk { chunk ->
|
|
if (chunk is IRLoopChunk) {
|
|
containsLoop = true
|
|
return@forEachChunk
|
|
}
|
|
val label = chunk.label
|
|
if (label != null)
|
|
chunkFirstIndex[label] = instrList.size
|
|
chunk.instructions.forEach { instrList.add(it) }
|
|
}
|
|
|
|
if (containsLoop || branchesBackwards(sub, instrList, chunkFirstIndex))
|
|
return oneSlotPerRegister(vregs, cursor)
|
|
|
|
val firstUse = mutableMapOf<Int, Int>()
|
|
val lastUse = mutableMapOf<Int, Int>()
|
|
instrList.forEachIndexed { index, instr ->
|
|
for (access in instr.registerAccesses) {
|
|
val num = access.register.num
|
|
firstUse[num] = minOf(firstUse[num] ?: index, index)
|
|
lastUse[num] = maxOf(lastUse[num] ?: index, index)
|
|
}
|
|
}
|
|
|
|
// greedy linear scan: a register takes the first free slot it fits in, otherwise a
|
|
// fresh slot below the locals; registers live at the same time can never share
|
|
val slots = mutableListOf<VregSlot>()
|
|
var c = cursor
|
|
val vregSlots = mutableMapOf<Int, Int>()
|
|
instrList.forEachIndexed { index, instr ->
|
|
for (regNum in instr.registerAccesses.map { it.register.num }.distinct().sorted()) {
|
|
if (firstUse[regNum] != index) continue
|
|
val size = slotSizeFor(vregs[regNum] ?: continue)
|
|
val alignment = when {
|
|
size >= 4 -> 4
|
|
size >= 2 -> 2
|
|
else -> 1
|
|
}
|
|
val slot = slots.firstOrNull { s ->
|
|
(s.occupant < 0 || lastUse[s.occupant]!! < index) && s.offset % alignment == 0 && s.size >= size
|
|
}
|
|
if (slot != null) {
|
|
slot.occupant = regNum
|
|
vregSlots[regNum] = slot.offset
|
|
} else {
|
|
c = c.floorDiv(alignment) * alignment
|
|
c -= size
|
|
vregSlots[regNum] = c
|
|
slots += VregSlot(c, size, regNum)
|
|
}
|
|
}
|
|
}
|
|
return c to vregSlots
|
|
}
|
|
|
|
/** true when any branch/jump instruction targets a label at or before its own position,
|
|
* a label outside this subroutine, an unresolvable label, or an indirect target */
|
|
private fun branchesBackwards(sub: IRSubroutine, instrList: List<IRInstruction>, chunkFirstIndex: Map<String, Int>): Boolean {
|
|
val subChunks = mutableSetOf<IRCodeChunkBase>()
|
|
sub.forEachChunk { subChunks.add(it) }
|
|
for ((index, instr) in instrList.withIndex()) {
|
|
// an indirect jump can target arbitrary code, including a backward edge;
|
|
// be conservative and treat it like a backward branch
|
|
if (instr.opcode == Opcode.JUMPI)
|
|
return true
|
|
val target = instr.target as? CodeReference.Label ?: continue
|
|
val targetChunk = program.resolveCodeTarget(target) ?: return true
|
|
if (targetChunk !in subChunks) return true
|
|
val targetIndex = chunkFirstIndex[target.name] ?: return true
|
|
if (targetIndex <= index) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun oneSlotPerRegister(vregs: Map<Int, IRDataType>, cursor: Int): Pair<Int, Map<Int, Int>> {
|
|
var c = cursor
|
|
val vregSlots = mutableMapOf<Int, Int>()
|
|
for ((regNum, irType) in vregs.entries.sortedBy { it.key }) {
|
|
val size = slotSizeFor(irType)
|
|
val alignment = when {
|
|
size >= 4 -> 4
|
|
size >= 2 -> 2
|
|
else -> 1
|
|
}
|
|
c = c.floorDiv(alignment) * alignment
|
|
c -= size
|
|
vregSlots[regNum] = c
|
|
}
|
|
return c to vregSlots
|
|
}
|
|
|
|
private fun slotSizeFor(irType: IRDataType): Int = when (irType) {
|
|
IRDataType.BYTE -> 1
|
|
IRDataType.WORD -> 2
|
|
IRDataType.FLOAT -> target.FLOAT_MEM_SIZE.toInt()
|
|
else -> target.POINTER_MEM_SIZE.toInt()
|
|
}
|
|
|
|
/**
|
|
* A re-entrant subroutine that keeps part of its state in program-static storage shares that
|
|
* storage between its live activations, which silently corrupts data. There is no sound way to
|
|
* compile that, so reject it: every variable it touches and every virtual register it uses must
|
|
* live in its stack frame.
|
|
*
|
|
* Only variables that the subroutine body itself uses are reported. A variable that is merely
|
|
* read by a defer handler holds no per-activation data; the compiler rejects defer in a
|
|
* recursive subroutine separately.
|
|
*/
|
|
private fun reportUnsoundStaticState(
|
|
framedSubs: Map<String, FrameLayoutResult>,
|
|
reentrant: Set<String>,
|
|
referencedIn: Map<String, Set<String>>,
|
|
addressTaken: Set<String>,
|
|
asmTexts: List<String>
|
|
) {
|
|
for (sub in program.allSubs()) {
|
|
if (sub.label !in reentrant) continue
|
|
// a %option noframe subroutine manages its own stack discipline by design;
|
|
// the programmer accepts that its overlapping runs share data
|
|
if (sub.noframe) continue
|
|
val framed = sub.label in framedSubs
|
|
val sharedVars = staticVarsOwnedBy(sub).filter { referencedIn[it]?.contains(sub.label) == true }
|
|
// a copy-in parameter keeps a static cell for its external references (§6.1),
|
|
// shared between overlapping runs of a re-entrant subroutine
|
|
val sharedCopyIns = (framedSubs[sub.label]?.copyInParams ?: emptySet()).filter { it !in sharedVars }
|
|
// an unframed subroutine also shares its virtual registers; a framed one relocated them all
|
|
val sharedVregs = if (framed) emptyList() else vregsUsedIn(sub).keys.toList()
|
|
if (sharedVars.isEmpty() && sharedVregs.isEmpty() && sharedCopyIns.isEmpty()) continue
|
|
val details = mutableListOf<String>()
|
|
for (varName in sharedVars) {
|
|
val v = program.st.lookup(varName) as? IRStStaticVariable
|
|
val why = v?.let { frameableVariableProblem(it, varName, addressTaken, asmTexts) }
|
|
?: if (framed) "it is shared with other code" else null
|
|
val name = "variable '${varName.substringAfterLast('.').removePrefix("p8v_")}'"
|
|
details += if (why == null) name else "$name ($why)"
|
|
}
|
|
for (paramName in sharedCopyIns) {
|
|
details += "parameter '${paramName.substringAfterLast('.').removePrefix("p8v_")}' (shared with other code)"
|
|
}
|
|
if (sharedVregs.isNotEmpty())
|
|
details += if (sharedVregs.size == 1) "1 temporary value" else "${sharedVregs.size} temporary values"
|
|
errors.err("subroutine '${userName(sub)}' cannot be compiled: it may be running twice at the same time, " +
|
|
"while sharing data between those runs: ${details.joinToString(", ")}. " +
|
|
"Restructure it so that each run has its own private data, " +
|
|
"or mark it %option noframe if the sharing is intentional",
|
|
sub.position)
|
|
}
|
|
}
|
|
|
|
/** the scoped IR label without its internal scope markers, for use in user-facing messages */
|
|
private fun userName(sub: IRSubroutine): String =
|
|
sub.label.split('.').joinToString(".") { it.removePrefix("p8b_").removePrefix("p8s_") }
|
|
|
|
/** the static variables that belong to this subroutine (the longest matching label prefix owns the name) */
|
|
private fun staticVarsOwnedBy(sub: IRSubroutine): List<String> {
|
|
val subLabels = program.allSubs().map { it.label }
|
|
fun ownerOf(varName: String): String? =
|
|
subLabels.filter { varName.startsWith("$it.") }.maxByOrNull { it.length }
|
|
return program.st.allVariables().map { it.name }.filter { ownerOf(it) == sub.label }.toList()
|
|
}
|
|
|
|
/**
|
|
* 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 ->
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
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}")
|
|
}
|
|
}
|
|
|
|
private fun slotOf(symbol: String?, slotBySymbol: Map<String, Int>, paramSlots: Map<String, Int>): Int? {
|
|
if (symbol == null) return null
|
|
return slotBySymbol[symbol] ?: paramSlots[symbol]
|
|
}
|
|
|
|
/** rewrite the argument locations of every call to a framed subroutine */
|
|
private fun rewriteCallArgumentLocations(framedSubs: Map<String, FrameLayoutResult>) {
|
|
if (framedSubs.isEmpty()) return
|
|
for (sub in program.allSubs()) {
|
|
val offsetsByCallee = mutableMapOf<String, List<Int>>()
|
|
for ((callee, layout) in framedSubs) {
|
|
val params = calleeParameters(callee) ?: continue
|
|
if (params.isEmpty()) continue
|
|
offsetsByCallee[callee] = params.mapIndexed { _, param ->
|
|
val scoped = if (param.startsWith("$callee.")) param else "$callee.$param"
|
|
layout.paramSlots[scoped] ?: 0
|
|
}
|
|
}
|
|
if (offsetsByCallee.isEmpty()) continue
|
|
sub.forEachChunk { chunk ->
|
|
val instrs = chunk.instructions
|
|
for (i in instrs.indices) {
|
|
val instr = instrs[i]
|
|
val site = instr.callSite ?: continue
|
|
val callee = (site.codeReference as? CodeReference.Label)?.name ?: continue
|
|
val offsets = offsetsByCallee[callee] ?: continue
|
|
if (site.arguments.size != offsets.size) continue
|
|
var changed = false
|
|
val rewritten = site.arguments.mapIndexed { argIndex, arg ->
|
|
if (arg.location is CallLocation.FrameSlot) {
|
|
arg
|
|
} else {
|
|
changed = true
|
|
arg.copy(location = CallLocation.FrameSlot(offsets[argIndex]))
|
|
}
|
|
}
|
|
if (changed)
|
|
instrs[i] = instr.copy(callSite = site.copy(arguments = rewritten))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun calleeParameters(callee: String): List<String>? =
|
|
program.allSubs().firstOrNull { it.label == callee }?.parameters?.map { it.name }
|
|
|
|
/** 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 (frameableVariableProblem(v, name, addressTaken, asmTexts) != null)
|
|
return null
|
|
// an array is described by its element data type, a scalar by its own
|
|
return if (v.dt.isArray) baseToScalar(v.dt.sub!!) else v.dt
|
|
}
|
|
|
|
/** like [isFrameableVariable], but an inline-assembly reference is acceptable: the parameter
|
|
* keeps its static cell and gets an entry-time copy-in from its incoming slot (§6.1) */
|
|
private fun isFrameableParameter(v: IRStStaticVariable, name: String, addressTaken: Set<String>, asmTexts: List<String>): DataType? {
|
|
if (frameableVariableProblem(v, name, addressTaken, asmTexts, asmAllowed = true) != null)
|
|
return null
|
|
// an array is described by its element data type, a scalar by its own
|
|
return if (v.dt.isArray) baseToScalar(v.dt.sub!!) else v.dt
|
|
}
|
|
|
|
/** returns why the variable cannot be moved into a frame, or null when it can */
|
|
private fun frameableVariableProblem(v: IRStStaticVariable, name: String, addressTaken: Set<String>, asmTexts: List<String>, asmAllowed: Boolean = false): String? {
|
|
if (v.shared) return "it is marked @shared" // @shared guarantees a stable, externally visible address
|
|
if (name in addressTaken) return "its address is taken"
|
|
if (!asmAllowed && isLabelReferencedInAsm(name, asmTexts)) return "inline assembly refers to it"
|
|
if (v.initializationValue != null && v.initializationValue !is IRVariableInitializer.Numeric) return "it has a static initializer"
|
|
if (v.align > 0u) return "it requests an explicit alignment"
|
|
if (v.zpwish == ZeropageWish.REQUIRE_ZEROPAGE || v.zpwish == ZeropageWish.PREFER_ZEROPAGE) return "it wants to be in the zeropage"
|
|
return when {
|
|
v.dt.isPointer -> null // pointer storage is always just an address
|
|
v.dt.isBasic && v.dt.isNumericOrBool -> null
|
|
v.dt.isArray && v.dt.sub != null && v.dt.sub != BaseDataType.POINTER -> {
|
|
val subDt = baseToScalar(v.dt.sub!!)
|
|
if (subDt == null) "it is not a plain numeric or boolean array"
|
|
else if (subDt.isStructInstance || subDt.isString || subDt.isPointer) "it is not plain data"
|
|
else null
|
|
}
|
|
else -> "it is not plain data"
|
|
}
|
|
}
|
|
|
|
/** true if [label] occurs as a whole symbol reference in any inline assembly text */
|
|
private fun isLabelReferencedInAsm(label: String, asmTexts: List<String>): Boolean {
|
|
// Avoid false positives where one label is a prefix/suffix of another (e.g. main.foo
|
|
// inside main.foobar). Treat label characters as letters, digits, underscore and dot.
|
|
val escaped = Regex.escape(label)
|
|
val pattern = "(?<![A-Za-z0-9_.])$escaped(?![A-Za-z0-9_.])".toRegex()
|
|
return asmTexts.any { pattern.containsMatchIn(it) }
|
|
}
|
|
|
|
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
|
|
BaseDataType.FLOAT -> DataType.FLOAT
|
|
else -> null
|
|
}
|
|
|
|
private fun irTypeFor(dt: DataType): IRDataType? = when {
|
|
dt.isByteOrBool -> IRDataType.BYTE
|
|
dt.isWord -> IRDataType.WORD
|
|
dt.isLong || dt.isPointer -> IRDataType.LONG
|
|
dt.isFloat -> IRDataType.FLOAT
|
|
else -> null
|
|
}
|
|
}
|