From f0bf290bac0ad3c4842948d25ac8567ce687567a Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Thu, 16 Jul 2026 20:59:34 +0200 Subject: [PATCH] Improved dead store optimization --- internal/optimizer/optimizer_test.go | 119 +++++++++++++++++++++++++-- internal/optimizer/pass_regdead.go | 25 +++++- 2 files changed, 131 insertions(+), 13 deletions(-) diff --git a/internal/optimizer/optimizer_test.go b/internal/optimizer/optimizer_test.go index eb74871..ec813df 100644 --- a/internal/optimizer/optimizer_test.go +++ b/internal/optimizer/optimizer_test.go @@ -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) + } +} diff --git a/internal/optimizer/pass_regdead.go b/internal/optimizer/pass_regdead.go index 90492a2..c41e512 100644 --- a/internal/optimizer/pass_regdead.go +++ b/internal/optimizer/pass_regdead.go @@ -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 }