Improved dead store optimization

This commit is contained in:
Mattias Hansson 2026-07-16 20:59:34 +02:00
parent b49ba67aac
commit f0bf290bac
2 changed files with 131 additions and 13 deletions

View file

@ -572,6 +572,7 @@ func TestPassRegDead_KeptStoreWithLoad(t *testing.T) {
}
func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
// sta regVar before a label — no lda regVar anywhere → globally dead
input := []string{
"\tlda #$05",
"\tsta myFunc_temp",
@ -586,11 +587,15 @@ func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
},
}
output, _ := Optimize(input, cfg)
output, dissolved := Optimize(input, cfg)
if !dissolved["myFunc_temp"] {
t.Error("expected myFunc_temp to be dissolved — globally dead")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta myFunc_temp") {
t.Errorf("expected sta myFunc_temp to be kept (label), got:\n%s", joined)
if strings.Contains(joined, "sta myFunc_temp") {
t.Errorf("expected sta myFunc_temp to be removed (globally dead), got:\n%s", joined)
}
}
@ -703,10 +708,9 @@ func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) {
}
}
func TestPassRegDead_CallSiteKeepsStore(t *testing.T) {
// sta regVar; jsr foo; ... no lda regVar ...; rts → store kept (conservative)
// The callee may modify A, so the store survives even though nothing
// reloads regVar after the call.
func TestPassRegDead_GloballyDeadBeforeJsr(t *testing.T) {
// sta regVar; jsr foo; no lda regVar anywhere → globally dead
// jsr does not protect a store with zero readers.
input := []string{
"\tlda 53281",
"\tsta call_val",
@ -723,13 +727,45 @@ func TestPassRegDead_CallSiteKeepsStore(t *testing.T) {
output, dissolved := Optimize(input, cfg)
if !dissolved["call_val"] {
t.Error("call_val should be dissolved — globally dead")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "sta call_val") {
t.Errorf("expected sta call_val to be removed (globally dead), got:\n%s", joined)
}
}
func TestPassRegDead_JsrProtectsStoreWhenReloaded(t *testing.T) {
// sta regVar; jsr foo; lda regVar → store kept
// lda exists (pre-scan passes), but jsr blocks local scan before reaching it.
// The callee may modify A, so the reload after jsr is genuine.
input := []string{
"\tlda 53281",
"\tsta call_val",
"\tjsr helper",
"\tlda call_val",
"\tsta 53280",
"\trts",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"call_val": true,
},
}
output, dissolved := Optimize(input, cfg)
if dissolved["call_val"] {
t.Error("call_val should NOT be dissolved — jsr is conservative")
t.Error("call_val should NOT be dissolved — lda exists, jsr protects store")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta call_val") {
t.Errorf("expected sta call_val to be kept (before jsr), got:\n%s", joined)
t.Errorf("expected sta call_val to be kept (jsr before reload), got:\n%s", joined)
}
}
@ -765,3 +801,68 @@ func TestPassRegDead_InitValueFlowsThrough(t *testing.T) {
t.Error("init value lda #$2a should survive")
}
}
func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
// sta regVar before a label, no lda regVar anywhere → globally dead
// Typical copy-loop pattern: the value flows through A, never reloaded.
input := []string{
"_LOOPSTART",
"\tlda (src),y",
"\tsta loop_val",
"\tsta (dst),y",
"\tbne _SKIP",
"_SKIP:",
"\tjmp _LOOPSTART",
"\trts",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"loop_val": true,
},
}
output, dissolved := Optimize(input, cfg)
if !dissolved["loop_val"] {
t.Error("loop_val should be dissolved — globally dead (no lda anywhere)")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "sta loop_val") {
t.Errorf("expected sta loop_val to be removed (globally dead), got:\n%s", joined)
}
}
func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
// sta regVar before a label, but lda regVar exists elsewhere → not globally dead
// Falls through to local scan. With jsr before the lda, store is kept.
input := []string{
"\tsta spill_me_bg",
"\tlda 53282",
"_SKIP:",
"\tjsr helper",
"\tlda spill_me_bg",
"\tsta 53282",
"\trts",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"spill_me_bg": true,
},
}
output, dissolved := Optimize(input, cfg)
if dissolved["spill_me_bg"] {
t.Error("spill_me_bg should NOT be dissolved — has a real lda")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta spill_me_bg") {
t.Errorf("expected sta spill_me_bg to be kept, got:\n%s", joined)
}
}

View file

@ -1,16 +1,33 @@
package optimizer
// passRegDead eliminates dead stores to REGISTER variables.
// A store is dead if no matching load (lda regVar) follows before A is
// clobbered or a label is reached. This is safe because REGISTER variables
// have a contract: nothing external can observe their memory location.
// 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 {
var result []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
}