c65gm/internal/optimizer/pass_regdead.go

88 lines
2.6 KiB
Go

package optimizer
// passRegDead eliminates dead stores to REGISTER variables.
// Two-phase approach:
// 1. Global pre-scan: if no lda regVar exists 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 lda,
// 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 lda?
hasLda := make(map[string]bool, len(registerVars))
for _, l := range lines {
if l.isCode && l.opcode == "lda" && l.operand != "" && registerVars[l.operand] {
hasLda[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 !hasLda[line.operand] {
// Globally dead — no lda 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 matching lda 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 && l.opcode == "lda" && l.operand == operand {
return false // the value IS read from memory later
}
}
return true // end of block reached with no matching load
}
// 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"
}