106 lines
3.2 KiB
Go
106 lines
3.2 KiB
Go
package optimizer
|
|
|
|
// passRegDead eliminates dead stores to REGISTER variables.
|
|
// Two-phase approach:
|
|
// 1. Global pre-scan: if no instruction reads regVar anywhere in the input,
|
|
// all stores to that variable are provably dead (labels are irrelevant).
|
|
// 2. Per-store local scan: for variables that DO have at least one read,
|
|
// scan forward through the current basic block.
|
|
//
|
|
// This is safe because REGISTER variables have a contract: nothing external
|
|
// can observe their memory location (ASM/Script/Macro references are compile errors).
|
|
func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
|
|
// Phase 1 — global pre-scan: which REGISTER vars have at least one read?
|
|
hasRead := make(map[string]bool, len(registerVars))
|
|
for _, l := range lines {
|
|
if l.isCode && l.operand != "" && registerVars[l.operand] && readsFrom(l.opcode, l.operand, l.operand) {
|
|
hasRead[l.operand] = true
|
|
}
|
|
}
|
|
|
|
// Phase 2 — per-store decision
|
|
var result []asmLine
|
|
for i := 0; i < len(lines); i++ {
|
|
line := lines[i]
|
|
|
|
if line.isCode && line.opcode == "sta" && line.operand != "" && registerVars[line.operand] {
|
|
if !hasRead[line.operand] {
|
|
// Globally dead — no read anywhere in the program
|
|
continue
|
|
}
|
|
if isRegStoreDead(lines, i+1, line.operand) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
result = append(result, line)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// isRegStoreDead scans forward from start to determine whether a store to a
|
|
// REGISTER variable operand is dead. Returns true if the store can be removed.
|
|
// A store is dead only if no instruction that reads the operand is found
|
|
// anywhere forward before a label or control-flow-ending instruction.
|
|
func isRegStoreDead(lines []asmLine, start int, operand string) bool {
|
|
for i := start; i < len(lines); i++ {
|
|
l := lines[i]
|
|
|
|
if l.optMarker || l.isComment {
|
|
continue
|
|
}
|
|
|
|
if l.isLabel {
|
|
return false
|
|
}
|
|
|
|
if l.isCode && isCallSite(l) {
|
|
return false // callee may modify A; conservatively keep the store
|
|
}
|
|
|
|
if l.isCode && blocksFlow(l) {
|
|
return true // rts/jmp/brk/rti — execution ends here
|
|
}
|
|
|
|
if l.isCode && readsFrom(l.opcode, l.operand, operand) {
|
|
return false // the value IS read from memory later
|
|
}
|
|
}
|
|
|
|
return true // end of block reached with no matching load
|
|
}
|
|
|
|
// readsFrom returns true if the instruction reads from the given memory operand.
|
|
// Covers loads (lda/ldx/ldy), compares (cmp/cpx/cpy), ALU ops (adc/sbc/and/ora/eor/bit),
|
|
// and read-modify-write instructions (dec/inc/asl/lsr/rol/ror).
|
|
func readsFrom(opcode, operand, varName string) bool {
|
|
if operand != varName {
|
|
return false
|
|
}
|
|
switch opcode {
|
|
case "lda", "ldx", "ldy":
|
|
return true
|
|
case "adc", "sbc", "and", "ora", "eor", "cmp", "cpx", "cpy", "bit":
|
|
return true
|
|
case "dec", "inc", "asl", "lsr", "rol", "ror":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// blocksFlow returns true for instructions that unconditionally end the current
|
|
// execution path: rts, jmp, brk, rti
|
|
func blocksFlow(line asmLine) bool {
|
|
switch line.opcode {
|
|
case "rts", "jmp", "brk", "rti":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isCallSite returns true for instructions that transfer control to a callee
|
|
// that may modify A, X, Y. The store before a call is conservatively kept.
|
|
func isCallSite(line asmLine) bool {
|
|
return line.opcode == "jsr"
|
|
}
|