IR: resolve loadx index-width type conflicts in usedRegisters; add conversion-collapse peephole

This commit is contained in:
Irmen de Jong committed 2026-08-23 02:41:12 +02:00
1 parent da6934ea33
commit 2ce7a63c1e
6 files changed
+239 -16

No files matched your search

@@ -49,6 +49,8 @@ class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA:
|| replaceConcatZeroMsbWithExt(chunk1, indexedInstructions)
|| removeDoubleLoadsAndStores(chunk1, indexedInstructions)
|| foldLoadStoremToStoreim(chunk1, indexedInstructions)
|| collapseConversions(chunk1, indexedInstructions)
|| deduplicateAddressComputations(chunk1, indexedInstructions)
|| removeUselessArithmetic(chunk1, indexedInstructions)
|| removeNeedlessCompares(chunk1, indexedInstructions)
|| removeWeirdBranches(chunk1, chunk2, indexedInstructions)
@@ -892,6 +894,194 @@ jump p8_label_gen_2
return changed
}
private fun collapseConversions(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
// Collapse adjacent conversions that compose into a single cheaper conversion:
// ext.b rX,rA + ext.w rY,rX -> extl.b rY,rA (zero-extends compose)
// exts.b rX,rA + ext.w rY,rX -> extl.b rY,rA (the outer zero-extend only sees the low
// byte that the inner instruction defined)
// ext.b rX,rA + exts.w rY,rX -> extl.b rY,rA (sign-extending a zero-extended byte
// yields the same as zero-extending it)
// exts.b rX,rA + exts.w rY,rX -> extls.b rY,rA (sign-extends compose)
// and truncation/widening pairs that cancel into a single mask:
// lsigw.l rX,rA + ext.w rY,rX -> loadr.l rY,rA ; and.l rY,#$ffff
// lsigb.? rX,rA + ext.b rY,rX -> loadr.? rY,rA ; and.? rY,#$ff
// Only applied when the intermediate register is not read anywhere else in this chunk.
if(chunk.instructions.size<2) return false
val readCounts = chunk.usedRegisters().readRegs.withDefault { 0 }
class Mod(val idx: Int, val replacement: IRInstruction?) // null replacement = delete
val mods = mutableListOf<Mod>()
val claimed = mutableSetOf<Int>()
for(k in 0 until indexedInstructions.size-1) {
val (i, inner) = indexedInstructions[k]
val (_, outer) = indexedInstructions[k+1]
val mid = inner.reg1
fun singleResult(opcode: Opcode) =
IRInstruction(opcode, IRDataType.BYTE, reg1 = outer.reg1, reg2 = inner.reg2)
if(i in claimed || i+1 in claimed)
continue
when {
// composed sign/zero extension chains on bytes
outer.opcode in setOf(Opcode.EXT, Opcode.EXTS) && outer.type==IRDataType.WORD
&& inner.opcode in setOf(Opcode.EXT, Opcode.EXTS) && inner.type==IRDataType.BYTE
&& mid!=null && mid==outer.reg2
&& readCounts[RegisterNum(mid)]==1 -> {
val newOp = if(inner.opcode==Opcode.EXTS && outer.opcode==Opcode.EXTS) Opcode.EXTLS else Opcode.EXTL
mods.add(Mod(i+1, null))
mods.add(Mod(i, singleResult(newOp)))
}
// truncate-long-to-word followed by zero-extend word-to-long == mask with $ffff
inner.opcode==Opcode.LSIGW && inner.type==IRDataType.LONG
&& outer.opcode==Opcode.EXT && outer.type==IRDataType.WORD
&& mid!=null && mid==outer.reg2 && inner.reg2!=null && outer.reg1!=null
&& readCounts[RegisterNum(mid)]==1 -> {
mods.add(Mod(i+1, IRInstruction(Opcode.AND, IRDataType.LONG, reg1 = outer.reg1, immediate = 0xffff)))
mods.add(Mod(i, IRInstruction(Opcode.LOADR, IRDataType.LONG, reg1 = outer.reg1, reg2 = inner.reg2)))
}
// truncate-long/word-to-byte followed by zero-extend byte-to-word == mask with $ff
inner.opcode==Opcode.LSIGB && inner.type in setOf(IRDataType.WORD, IRDataType.LONG)
&& outer.opcode==Opcode.EXT && outer.type==IRDataType.BYTE
&& mid!=null && mid==outer.reg2 && inner.reg2!=null && outer.reg1!=null
&& readCounts[RegisterNum(mid)]==1 -> {
mods.add(Mod(i+1, IRInstruction(Opcode.AND, inner.type, reg1 = outer.reg1, immediate = 0xff)))
mods.add(Mod(i, IRInstruction(Opcode.LOADR, inner.type, reg1 = outer.reg1, reg2 = inner.reg2)))
}
else -> continue
}
claimed.add(i)
claimed.add(i+1)
}
var changed = false
for(mod in mods.sortedByDescending { it.idx }) {
if(mod.replacement==null) {
chunk.instructions.removeAt(mod.idx)
changed = true
}
else {
chunk.instructions[mod.idx] = mod.replacement
changed = true
}
}
return changed
}
private fun deduplicateAddressComputations(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
// IR3: collapse repeated buf+i address computations inside one chunk.
// Looks for the 4-instruction pattern that computes buf+i into a pointer:
// loadm.p ptrReg, buf
// loadm.w/b idxReg, idxVar
// ext* extReg, idxReg (any EXT/EXTS/EXTL/EXTLS)
// addr.p destReg, otherReg where destReg/otherReg are ptrReg and extReg (either order)
// If two such groups with identical (buf, idxVar) occur in the same chunk with no barrier
// between them, the second group is replaced by a single loadr.p copy from the first group's dest.
if(chunk.instructions.size < 8) return false
data class AddrGroup(
val startIdx: Int,
val ptrReg: Int,
val idxReg: Int,
val extReg: Int,
val destReg: Int,
val bufKey: String,
val idxKey: String
)
fun memKey(ins: IRInstruction): String {
// labelSymbol/labelSymbolOffset/address/type uniquely identify the variable
return "${ins.type}:${ins.labelSymbol}:${ins.labelSymbolOffset}:${ins.address}"
}
fun writesReg(ins: IRInstruction, reg: Int): Boolean {
val formats = instructionFormats[ins.opcode] ?: return false
val fmt = formats[ins.type] ?: formats[null] ?: return false
if((fmt.reg1 == OperandDirection.WRITE || fmt.reg1 == OperandDirection.READWRITE) && ins.reg1 == reg) return true
if((fmt.reg2 == OperandDirection.WRITE || fmt.reg2 == OperandDirection.READWRITE) && ins.reg2 == reg) return true
if((fmt.reg3 == OperandDirection.WRITE || fmt.reg3 == OperandDirection.READWRITE) && ins.reg3 == reg) return true
// fp regs are separate, not relevant for pointer regs
return false
}
fun writesMem(ins: IRInstruction, memKey: String): Boolean {
if(memKey == "null:null:null:null") return false
if(memKey(ins) != memKey) return false
val formats = instructionFormats[ins.opcode] ?: return false
val fmt = formats[ins.type] ?: formats[null] ?: return false
return fmt.address == OperandDirection.WRITE || fmt.address == OperandDirection.READWRITE
}
val groups = mutableListOf<AddrGroup>()
var i = 0
while(i <= chunk.instructions.size - 4) {
val a = chunk.instructions[i]
val b = chunk.instructions[i+1]
val c = chunk.instructions[i+2]
val d = chunk.instructions[i+3]
val isA = a.opcode == Opcode.LOADM && a.type == IRDataType.POINTER && a.reg1 != null
val isB = b.opcode == Opcode.LOADM && b.type in setOf(IRDataType.BYTE, IRDataType.WORD) && b.reg1 != null
val isC = c.opcode in setOf(Opcode.EXT, Opcode.EXTS, Opcode.EXTL, Opcode.EXTLS) && c.reg1 != null && c.reg2 == b.reg1
val isD = d.opcode == Opcode.ADDR && d.type == IRDataType.POINTER && d.reg1 != null && d.reg2 != null
if(isA && isB && isC && isD) {
val ptrReg = a.reg1!!
val extReg = c.reg1!!
val d1 = d.reg1!!
val d2 = d.reg2!!
val destOk = (d1 == ptrReg && d2 == extReg) || (d1 == extReg && d2 == ptrReg)
if(destOk) {
groups.add(AddrGroup(i, ptrReg, b.reg1!!, extReg, d1, memKey(a), memKey(b)))
i += 4
continue
}
}
i++
}
if(groups.size < 2) return false
// For each later group, find earliest earlier group with same (buf,idx) and no barrier; replace it
class Mod(val idx: Int, val replacement: IRInstruction?)
val mods = mutableListOf<Mod>()
val claimed = mutableSetOf<Int>()
for(j in groups.indices) {
val gj = groups[j]
if(gj.startIdx in claimed) continue
// find earliest i<j with same keys
var found: AddrGroup? = null
for(k in 0 until j) {
val gk = groups[k]
if(gk.startIdx in claimed) continue // don't use a replaced group as source; keep earliest non-replaced
if(gk.bufKey != gj.bufKey || gk.idxKey != gj.idxKey) continue
// barrier check between gk and gj
var barrier = false
for(t in gk.startIdx + 4 until gj.startIdx) {
if(t in claimed) continue // already slated for removal, ignore? but we haven't applied mods yet
val ins = chunk.instructions[t]
if(writesReg(ins, gk.destReg)) { barrier = true; break }
if(writesMem(ins, gk.bufKey) || writesMem(ins, gk.idxKey)) { barrier = true; break }
}
if(!barrier) { found = gk; break }
}
if(found != null) {
// replace gj's 4 insns with single loadr.p
if(gj.startIdx in claimed || gj.startIdx+1 in claimed || gj.startIdx+2 in claimed || gj.startIdx+3 in claimed) continue
mods.add(Mod(gj.startIdx + 3, null))
mods.add(Mod(gj.startIdx + 2, null))
mods.add(Mod(gj.startIdx + 1, null))
mods.add(Mod(gj.startIdx, IRInstruction(Opcode.LOADR, IRDataType.POINTER, reg1 = gj.destReg, reg2 = found.destReg)))
claimed.add(gj.startIdx); claimed.add(gj.startIdx+1); claimed.add(gj.startIdx+2); claimed.add(gj.startIdx+3)
}
}
if(mods.isEmpty()) return false
for(mod in mods.sortedByDescending { it.idx }) {
if(mod.replacement == null) chunk.instructions.removeAt(mod.idx)
else chunk.instructions[mod.idx] = mod.replacement
}
return true
}
private fun removeDeadStores(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
// Detect and remove dead stores: when a register is written but never read before being overwritten.
// Example:
@@ -89,7 +89,8 @@ class IRUnusedCodeRemover(
val writeRegsCounts = mutableMapOf<RegisterNum, Int>()
val writeFpRegsCounts = mutableMapOf<RegisterNum, Int>()
val regsTypes = mutableMapOf<RegisterNum, IRDataType>()
ins.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes, null, indexRegType)
val indexGuessRegs = mutableSetOf<RegisterNum>()
ins.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes, indexGuessRegs, null, indexRegType)
readRegs.addAll(readRegsCounts.keys.map { it.value })
readFpRegs.addAll(readFpRegsCounts.keys.map { it.value })
}
@@ -260,8 +260,8 @@ private fun optimizeTailCall(linesBy: Sequence<List<TrimmedLine>>): List<Modific
} ?: continue
if (secondInstr == "rts") {
val target = firstInstr.substringAfter("$callMnemonic ").trim()
// Skip indirect calls like bsr (a0) or jsr (a6)
if (target.startsWith("("))
// Skip indirect/library calls like bsr (a0), jsr (a0) or jsr dos.VPrintf(a6)
if ('(' in target)
continue
val indent = lines[0].value.takeWhile { it.isWhitespace() }
if (hasLabel(lines[0].trimmed)) {
+3
View File
@@ -47,6 +47,9 @@ Romable (%option romable)
IR/VM
^^^^^
- encode indexed scaling into IR (so that m68k codegen can use scale factor addressing) see ideas/scaled-indexing-IR.md
- **Index register width guessing in register type metadata**: ``LOADX``/``STOREX``/``STOREZX`` do not encode the width of their index register, so ``determineReg1Type``/``determineReg2Type`` (``IRInstructions.kt``) guess it from a caller-supplied ``indexRegType`` (byte on 16-bit-pointer targets, word on 32-bit-pointer ones). But the IR generator actually reuses registers of any width as index (e.g. a uword loop counter that doubles as a ``loadx.b`` index), so the guess can contradict the register's one-and-only allocated type from ``RegisterPool``. This currently throws "register given multiple types" when chunk metadata is recomputed (see ``setRegType``). Workaround in place: guessed typings are tracked in ``indexGuessRegs`` and never conflict with definitive ones. Cleaner alternatives (pick one):
- make ``IRCodeGen`` always widen the index to the canonical ``indexRegType`` right before emitting ``loadx``/``storex``/``storezx``, so the guess is always true; or
- stop recording guessed types altogether and thread the authoritative ``RegisterPool`` types into ``usedRegisters()`` so the metadata layer only reports facts. Note: the m68k regfile layout computation requires every used register to have a recorded type, so simply returning ``null`` for these cases breaks slot sizing.
- maybe change all branch instructions to have 2 exits (label if branch condition true, and label if false) instead of 1, and get rid of the implicit "next code chunk" link between chunks.
- implement more TODOs in AssignmentGen?
- add even more optimizations in IRPeepholeOptimizer?
@@ -1042,6 +1042,7 @@ data class IRInstruction(
readFpRegsCounts: MutableMap<RegisterNum, Int>,
writeFpRegsCounts: MutableMap<RegisterNum, Int>,
regsTypes: MutableMap<RegisterNum, IRDataType>,
indexGuessRegs: MutableSet<RegisterNum>,
chunk: IRCodeChunk?,
indexRegType: IRDataType = IRDataType.BYTE
) {
@@ -1049,44 +1050,67 @@ data class IRInstruction(
fun incWriteReg(reg: RegisterNum) = writeRegsCounts.merge(reg, 1, Int::plus)
fun incReadFp(reg: RegisterNum) = readFpRegsCounts.merge(reg, 1, Int::plus)
fun incWriteFp(reg: RegisterNum) = writeFpRegsCounts.merge(reg, 1, Int::plus)
fun setRegType(reg: RegisterNum, type: IRDataType) {
// For LOADX/STOREX/STOREZX the width of the index register is not encoded in the
// instruction itself; it is guessed from the target's pointer size (indexRegType).
// That guess can legitimately differ from the register's one-and-only allocated type
// (for instance a word loop counter that also serves as a loadx index). Index-register
// type recordings are therefore treated as guesses, tracked across the whole scan in
// indexGuessRegs: they never overwrite nor contradict a definitively-known type, and a
// definitive type always replaces an earlier guess.
fun setRegType(reg: RegisterNum, type: IRDataType, isIndexGuess: Boolean = false) {
val existingType = regsTypes[reg]
if (existingType != null && existingType != type) {
if (existingType == null) {
regsTypes[reg] = type
if(isIndexGuess) indexGuessRegs.add(reg)
} else if (existingType != type) {
// POINTER is compatible with WORD or LONG (size depends on target)
val compatible = (existingType==IRDataType.POINTER && type in setOf(IRDataType.WORD, IRDataType.LONG)) ||
(type==IRDataType.POINTER && existingType in setOf(IRDataType.WORD, IRDataType.LONG))
if(!compatible)
throw IllegalArgumentException("register $reg given multiple types! $existingType and $type while processing $this in $chunk")
} else
regsTypes[reg] = type
val existingIsGuess = reg in indexGuessRegs
when {
compatible -> {} // keep first recording, as before
isIndexGuess -> {} // never contradict a definitive type
existingIsGuess -> { // definitive replaces earlier guess
regsTypes[reg] = type
indexGuessRegs.remove(reg)
}
else -> throw IllegalArgumentException("register $reg given multiple types! $existingType and $type while processing $this in $chunk")
}
}
}
fun reg1IsIndexGuess() =
(this.type == IRDataType.FLOAT && opcode in setOf(Opcode.LOADX, Opcode.STOREX, Opcode.STOREZX)) ||
(opcode == Opcode.STOREZX)
when (this.reg1direction) {
OperandDirection.UNUSED -> {}
OperandDirection.READ -> {
incReadReg(RegisterNum(this.reg1!!))
determineReg1Type(indexRegType)?.let { setRegType(RegisterNum(this.reg1), it) }
determineReg1Type(indexRegType)?.let { setRegType(RegisterNum(this.reg1), it, reg1IsIndexGuess()) }
}
OperandDirection.WRITE -> {
incWriteReg(RegisterNum(this.reg1!!))
determineReg1Type(indexRegType)?.let { setRegType(RegisterNum(this.reg1), it) }
determineReg1Type(indexRegType)?.let { setRegType(RegisterNum(this.reg1), it, reg1IsIndexGuess()) }
}
OperandDirection.READWRITE -> {
incReadReg(RegisterNum(this.reg1!!))
incWriteReg(RegisterNum(this.reg1))
determineReg1Type(indexRegType)?.let { setRegType(RegisterNum(this.reg1), it) }
determineReg1Type(indexRegType)?.let { setRegType(RegisterNum(this.reg1), it, reg1IsIndexGuess()) }
}
}
val reg2IsIndexGuess = opcode == Opcode.LOADX || opcode == Opcode.STOREX
when (this.reg2direction) {
OperandDirection.UNUSED -> {}
OperandDirection.READ -> {
incReadReg(RegisterNum(this.reg2!!))
determineReg2Type(indexRegType)?.let { setRegType(RegisterNum(this.reg2), it) }
determineReg2Type(indexRegType)?.let { setRegType(RegisterNum(this.reg2), it, reg2IsIndexGuess) }
}
OperandDirection.READWRITE -> {
incReadReg(RegisterNum(this.reg2!!))
incWriteReg(RegisterNum(this.reg2))
determineReg2Type(indexRegType)?.let { setRegType(RegisterNum(this.reg2), it) }
determineReg2Type(indexRegType)?.let { setRegType(RegisterNum(this.reg2), it, reg2IsIndexGuess) }
}
else -> throw IllegalArgumentException("reg2 can only be read or readwrite")
}
@@ -318,6 +318,7 @@ class IRProgram(val name: String,
// on 32-bit targets LOADX/STOREX/STOREZX use a word index register (0-32767) instead of a byte (0-255)
val indexRegType = if(options.compTarget.POINTER_MEM_SIZE > 2u) IRDataType.WORD else IRDataType.BYTE
val globalInitIndexGuessRegs = mutableSetOf<RegisterNum>()
globalInits.instructions.forEach {
it.addUsedRegistersCounts(
readRegsCounts,
@@ -325,6 +326,7 @@ class IRProgram(val name: String,
readFpRegsCounts,
writeFpRegsCounts,
regsTypes,
globalInitIndexGuessRegs,
globalInits,
indexRegType
)
@@ -605,9 +607,10 @@ class IRCodeChunk(label: String?, next: IRCodeChunkBase?): IRCodeChunkBase(label
val readRegsCounts = mutableMapOf<RegisterNum, Int>().withDefault { 0 }
val regsTypes = mutableMapOf<RegisterNum, IRDataType>()
val readFpRegsCounts = mutableMapOf<RegisterNum, Int>().withDefault { 0 }
val writeRegsCounts = mutableMapOf<RegisterNum, Int>().withDefault { 0 }
val writeFpRegsCounts = mutableMapOf<RegisterNum, Int>().withDefault { 0 }
instructions.forEach { it.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes, this, indexRegType) }
val writeRegsCounts = mutableMapOf<RegisterNum, Int>().withDefault { 0 }
val indexGuessRegs = mutableSetOf<RegisterNum>()
instructions.forEach { it.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes, indexGuessRegs, this, indexRegType) }
return RegistersUsed(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes)
}
@@ -713,6 +716,7 @@ private fun registersUsedInAssembly(isIR: Boolean, assembly: String): RegistersU
val writeFpRegsCounts = mutableMapOf<RegisterNum, Int>().withDefault { 0 }
if(isIR) {
val indexGuessRegs = mutableSetOf<RegisterNum>()
assembly.lineSequence().forEach { line ->
val t = line.trim()
if(t.isNotEmpty()) {
@@ -724,6 +728,7 @@ private fun registersUsedInAssembly(isIR: Boolean, assembly: String): RegistersU
readFpRegsCounts,
writeFpRegsCounts,
regsTypes,
indexGuessRegs,
null
)
is ParsedIRLine.Label -> { /* labels can be skipped */ }