package optimizer import ( "strings" "testing" ) func lines(s ...string) []SourceLine { out := make([]SourceLine, len(s)) for i, t := range s { out[i] = SourceLine{Text: t, Origin: OriginGenerated} } return out } func verbatimLines(s ...string) []SourceLine { out := make([]SourceLine, len(s)) for i, t := range s { out[i] = SourceLine{Text: t, Origin: OriginAsm} } return out } func TestPassLoadElimination(t *testing.T) { tests := []struct { name string input []SourceLine expected int // expected number of lines after optimization }{ { name: "redundant same variable", input: lines("\tlda x", "\tsta y", "\tlda x"), expected: 2, // third lda removed }, { name: "not redundant - store to same variable", input: lines("\tlda x", "\tsta x", "\tlda x"), expected: 3, // X was stored, so A is stale }, { name: "not redundant - arithmetic in between", input: lines("\tlda x", "\tadc #1", "\tlda x"), expected: 3, // A was modified }, { name: "redundant - label resets state", input: lines("\tlda x", "label", "\tlda x"), expected: 3, // label resets state, second lda not redundant }, { name: "x register redundant", input: lines("\tldx counter", "\tldy counter", "\tldx counter"), expected: 2, // third ldx is redundant (X still holds counter) }, { name: "immediate loads not affected", input: lines("\tlda #5", "\tsta x", "\tlda #5"), expected: 3, // handled by imm pass }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parsed := parseLines(tt.input) result := passLoadElimination(parsed, nil) cleaned := stripOptMarkers(result) if got := len(cleaned); got != tt.expected { t.Errorf("got %d lines, want %d\ninput: %v\noutput: %v", got, tt.expected, tt.input, linesToString(cleaned)) } }) } } func TestPassImmElimination(t *testing.T) { tests := []struct { name string input []SourceLine expected int }{ { name: "redundant same immediate", input: lines("\tlda #$05", "\tsta y", "\tlda #$05"), expected: 2, // third removed }, { name: "different immediate values", input: lines("\tlda #5", "\tsta y", "\tlda #3"), expected: 3, // different values }, { name: "arithmetic invalidates", input: lines("\tlda #5", "\tadc #1", "\tlda #5"), expected: 3, // adc modified A }, { name: "x and y independent", input: lines("\tldx #$00", "\tldy #$00", "\tldx #$00"), expected: 2, // first ldx removed? No, let me check: ldx #0 then ldy #0, X still 0, so third redundant // Actually: ldx#0(X=0), ldy#0(Y=0,X=0), ldx#0(X=0,redundant) // Result: ldx#0, ldy#0 → 2 lines }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parsed := parseLines(tt.input) result := passImmElimination(parsed) cleaned := stripOptMarkers(result) if got := len(cleaned); got != tt.expected { t.Errorf("got %d lines, want %d\ninput: %v\noutput: %v", got, tt.expected, tt.input, linesToString(cleaned)) } }) } } func TestPassJmpNext(t *testing.T) { tests := []struct { name string input []SourceLine expected int }{ { name: "jmp to next label", input: lines("\tjmp _L1", "_L1"), expected: 1, // jmp removed }, { name: "jmp not to next label", input: lines("\tjmp _L1", "\tlda x", "_L1"), expected: 3, // not immediately followed by label }, { name: "label different from jmp target", input: lines("\tjmp _L1", "_L2"), expected: 2, // different labels }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parsed := parseLines(tt.input) result := passJmpNext(parsed) cleaned := stripOptMarkers(result) if got := len(cleaned); got != tt.expected { t.Errorf("got %d lines, want %d\ninput: %v\noutput: %v", got, tt.expected, tt.input, linesToString(cleaned)) } }) } } func TestPassSelfAssignment(t *testing.T) { tests := []struct { name string input []SourceLine expected int }{ { name: "self assignment", input: lines("\tlda x", "\tsta x"), expected: 0, // both removed }, { name: "not self - different variables", input: lines("\tlda x", "\tsta y"), expected: 2, // different }, { name: "not self - immediate load", input: lines("\tlda #$05", "\tsta #$05"), expected: 2, // immediate + immediate doesn't match }, { name: "not self - SM code pattern (+ offset)", input: lines("\tlda x", "\tsta _L1+1"), expected: 2, // SM code pattern }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parsed := parseLines(tt.input) result := passSelfAssignment(parsed) cleaned := stripOptMarkers(result) if got := len(cleaned); got != tt.expected { t.Errorf("got %d lines, want %d\ninput: %v\noutput: %v", got, tt.expected, tt.input, linesToString(cleaned)) } }) } } func TestPassLoadIO(t *testing.T) { cfg := &Config{} cfg.IOMap[0xD012] = true // raster line register — changes constantly t.Run("IO load not eliminated", func(t *testing.T) { parsed := parseLines(lines("\tlda $D012", "\tlda $D012")) result := passLoadElimination(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 2 { t.Errorf("expected 2 lines (IO skip), got %d", len(cleaned)) } }) t.Run("non-IO load eliminated", func(t *testing.T) { parsed := parseLines(lines("\tlda $C000", "\tlda $C000")) result := passLoadElimination(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 1 { t.Errorf("expected 1 line (non-IO), got %d", len(cleaned)) } }) t.Run("decimal IO load not eliminated", func(t *testing.T) { parsed := parseLines(lines("\tlda 53266", "\tlda 53266")) result := passLoadElimination(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 2 { t.Errorf("expected 2 lines (decimal IO skip), got %d", len(cleaned)) } }) t.Run("variable name not caught by IO", func(t *testing.T) { parsed := parseLines(lines("\tlda RASTER_LINE", "\tlda RASTER_LINE")) result := passLoadElimination(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 1 { t.Errorf("expected 1 line (var not IO), got %d", len(cleaned)) } }) } func TestPassLoadNoCommentReset(t *testing.T) { // Comments should NOT reset register state parsed := parseLines(lines( "\tlda b", "\tsta a", "; comment", "\tlda b", // should be redundant )) result := passLoadElimination(parsed, nil) if len(result) != 3 { t.Errorf("expected 3 lines (comment kept, lda b removed), got %d:\n%v", len(result), linesToString(result)) } } func TestOptimizeIntegration(t *testing.T) { input := lines( "; a = b", "; @@OPT:LINEAR:LET", "\tlda b", "\tsta a", "; second use of b (redundant load)", "; @@OPT:LINEAR:LET", "\tlda b", "\tsta c", ) cfg := &Config{EnableLoad: true} output, _ := Optimize(input, cfg) // 2 source comments + 3 asm lines (lda b removed) = 5 if len(output) != 5 { t.Errorf("expected 5 lines, got %d:\n%v", len(output), output) } } func TestOptimizeStoreTransferIntegration(t *testing.T) { input := lines( "\tlda #$05", "\tsta x", "\tldy x", "\tlda (zp),y", ) cfg := &Config{EnableStoreLoad: true} output, _ := Optimize(input, cfg) if len(output) != 4 { t.Fatalf("expected 4 lines, got %d:\n%v", len(output), output) } if output[2] != "\ttay" { t.Errorf("expected ldy x to become tay via Optimize, got %q", output[2]) } } func TestOptimizeWithDebug(t *testing.T) { input := lines( "\tlda x", "; @@OPT:LINEAR:LET", "\tlda x", "\tsta y", ) cfg := &Config{EnableLoad: true, Debug: true} output, _ := Optimize(input, cfg) // Header + 2 kept lines + 1 removed annotation = 4 if len(output) != 4 { t.Errorf("expected 4 lines, got %d:\n%v", len(output), output) } if !containsPrefix(output, "; --- peephole:") { t.Errorf("expected peephole summary line:\n%v", output) } if !containsSubstring(output, "[removed]") { t.Errorf("expected [removed] annotation:\n%v", output) } } func containsPrefix(lines []string, prefix string) bool { for _, l := range lines { if len(l) >= len(prefix) && l[:len(prefix)] == prefix { return true } } return false } func containsSubstring(lines []string, sub string) bool { for _, l := range lines { for i := 0; i <= len(l)-len(sub); i++ { if l[i:i+len(sub)] == sub { return true } } } return false } func TestPassStoreReload(t *testing.T) { cfg := &Config{} tests := []struct { name string input []SourceLine expected int }{ { name: "sta x then lda x", input: lines("\tsta x", "\tlda x"), expected: 1, }, { name: "different variables", input: lines("\tsta x", "\tlda y"), expected: 2, }, { name: "ldy between sta and lda", input: lines("\tsta x", "\tldy #0", "\tlda x"), expected: 2, }, { name: "adc between — modifies A", input: lines("\tsta x", "\tadc #1", "\tlda x"), expected: 3, }, { name: "label between — barrier", input: lines("\tsta x", "label", "\tlda x"), expected: 3, }, { name: "different registers", input: lines("\tsta x", "\tldx x"), expected: 2, }, { name: "comment between", input: lines("\tsta x", "; source line", "\tlda x"), expected: 2, }, { name: "comment and ldy between", input: lines("\tsta x", "; POKE addr", "\tldy #5", "\tlda x"), expected: 3, }, { name: "SM code — operand with +", input: lines("\tsta _L1+1", "\tlda _L1+1"), expected: 2, }, { name: "indexed store — operand with (", input: lines("\tsta (zp),y", "\tlda (zp),y"), expected: 2, }, { name: "indexed store — operand with ,", input: lines("\tsta $D020,x", "\tlda $D020,x"), expected: 2, }, { name: "different operand offsets", input: lines("\tsta x", "\tlda x+1"), expected: 2, }, { name: "inx between — does not modify A", input: lines("\tsta x", "\tinx", "\tlda x"), expected: 2, }, { name: "dex between — does not modify A", input: lines("\tsta x", "\tdex", "\tlda x"), expected: 2, }, { name: "iny between — does not modify A", input: lines("\tsta x", "\tiny", "\tlda x"), expected: 2, }, { name: "dey between — does not modify A", input: lines("\tsta x", "\tdey", "\tlda x"), expected: 2, }, { name: "tax between — does not modify A", input: lines("\tsta x", "\ttax", "\tlda x"), expected: 2, }, { name: "tay between — does not modify A", input: lines("\tsta x", "\ttay", "\tlda x"), expected: 2, }, { name: "txa between — modifies A, keep lda", input: lines("\tsta x", "\ttxa", "\tlda x"), expected: 3, }, { name: "tya between — modifies A, keep lda", input: lines("\tsta x", "\ttya", "\tlda x"), expected: 3, }, { name: "pla between — modifies A, keep lda", input: lines("\tsta x", "\tpla", "\tlda x"), expected: 3, }, { name: "multiple stores — pick the last sta", input: lines("\tsta x", "\tsta y", "\tlda x"), expected: 2, // sta x kept, sta y kept, lda x removed }, { name: "@@OPT marker between — skip, lda removed", input: lines("\tsta x", "; @@OPT:LINEAR:LET", "\tlda x"), expected: 1, // @@OPT stripped by stripOptMarkers }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parsed := parseLines(tt.input) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if got := len(cleaned); got != tt.expected { t.Errorf("got %d lines, want %d\ninput: %v\noutput: %v", got, tt.expected, tt.input, linesToString(cleaned)) } }) } } func TestPassStoreReloadIO(t *testing.T) { t.Run("c64 border color", func(t *testing.T) { cfg := &Config{} cfg.IOMap[0xD020] = true parsed := parseLines(lines("\tsta $D020", "\tlda $D020")) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 2 { t.Errorf("expected 2 lines (I/O skip), got %d", len(cleaned)) } }) t.Run("non-IO address", func(t *testing.T) { cfg := &Config{} cfg.IOMap[0xD020] = true parsed := parseLines(lines("\tsta $C000", "\tlda $C000")) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 1 { t.Errorf("expected 1 line (non-IO), got %d", len(cleaned)) } }) t.Run("decimal address in IO range", func(t *testing.T) { cfg := &Config{} cfg.IOMap[0xD020] = true // Decimal 53280 = $D020, now caught by IOMap regardless of base parsed := parseLines(lines("\tsta 53280", "\tlda 53280")) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 2 { t.Errorf("expected 2 lines (decimal I/O protected), got %d", len(cleaned)) } }) t.Run("variable name in IO region", func(t *testing.T) { cfg := &Config{} cfg.IOMap[0xD020] = true // Variable names like VIC_BORDER aren't checked against IOMap parsed := parseLines(lines("\tsta BORDER_COLOR", "\tlda BORDER_COLOR")) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 1 { t.Errorf("expected 1 line (var name not I/O), got %d", len(cleaned)) } }) t.Run("decimal address not in IO range", func(t *testing.T) { cfg := &Config{} cfg.IOMap[0xD020] = true parsed := parseLines(lines("\tsta 49152", "\tlda 49152")) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 1 { t.Errorf("expected 1 line (decimal not IO), got %d", len(cleaned)) } }) t.Run("IOMap from AddIORegions", func(t *testing.T) { cfg := &Config{} cfg.AddIORegions([]IORegion{{Start: 0xD000, End: 0xDFFF}}) parsed := parseLines(lines("\tsta $D020", "\tlda $D020")) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != 2 { t.Errorf("expected 2 lines (I/O via AddIORegions), got %d", len(cleaned)) } }) t.Run("multiple IO regions", func(t *testing.T) { cfg := &Config{} cfg.AddIORegions([]IORegion{ {Start: 0xD000, End: 0xDFFF}, {Start: 0xDC00, End: 0xDC0F}, }) tests := []struct { addr string expect int }{ {"$D020", 2}, {"$DC00", 2}, {"$DC0F", 2}, {"$C000", 1}, {"$E000", 1}, } for _, tt := range tests { parsed := parseLines(lines("\tsta "+tt.addr, "\tlda "+tt.addr)) result := passStoreReload(parsed, cfg) cleaned := stripOptMarkers(result) if len(cleaned) != tt.expect { t.Errorf("addr %s: expected %d lines, got %d", tt.addr, tt.expect, len(cleaned)) } } }) } func TestPassStoreTransfer(t *testing.T) { cfg := &Config{} t.Run("sta x then ldy x becomes tay", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "\tldy x")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[0] != "\tsta x" || out[1] != "\ttay" { t.Errorf("got %v", out) } }) t.Run("sta x then ldx x becomes tax", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "\tldx x")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[0] != "\tsta x" || out[1] != "\ttax" { t.Errorf("got %v", out) } }) t.Run("comment between is skipped", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "; source", "\tldy x")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 3 || out[2] != "\ttay" { t.Errorf("got %v", out) } }) t.Run("label between blocks transfer", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "label", "\tldy x")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 3 || out[2] != "\tldy x" { t.Errorf("got %v", out) } }) t.Run("IO address not transferred", func(t *testing.T) { ioCfg := &Config{} ioCfg.IOMap[0xD020] = true parsed := parseLines(lines("\tsta $D020", "\tldy $D020")) result := passStoreTransfer(parsed, ioCfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[1] != "\tldy $D020" { t.Errorf("got %v", out) } }) t.Run("operand mismatch no transfer", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "\tldy y")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[1] != "\tldy y" { t.Errorf("got %v", out) } }) t.Run("inline comment on load blocks transfer", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "\tldy x ; note")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[1] != "\tldy x ; note" { t.Errorf("got %v", out) } }) t.Run("indexed store not transferred", func(t *testing.T) { parsed := parseLines(lines("\tsta (zp),y", "\tldy (zp),y")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[1] != "\tldy (zp),y" { t.Errorf("got %v", out) } }) t.Run("lda between blocks transfer", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "\tlda y", "\tldy x")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 3 || out[2] != "\tldy x" { t.Errorf("got %v", out) } }) t.Run("sta x then lda x not transferred", func(t *testing.T) { parsed := parseLines(lines("\tsta x", "\tlda x")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[1] != "\tlda x" { t.Errorf("got %v", out) } }) t.Run("sta as last line no transfer", func(t *testing.T) { parsed := parseLines(lines("\tlda #1", "\tsta x")) result := passStoreTransfer(parsed, cfg) out := linesToString(stripOptMarkers(result)) if len(out) != 2 || out[1] != "\tsta x" { t.Errorf("got %v", out) } }) } func TestPassRegDead_DeadStore(t *testing.T) { input := lines( "\tlda #$05", "\tsta myFunc_temp", "\tsta $d020", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "myFunc_temp": true, }, } output, dissolved := Optimize(input, cfg) if !dissolved["myFunc_temp"] { t.Error("expected myFunc_temp to be dissolved") } joined := strings.Join(output, "\n") if strings.Contains(joined, "myFunc_temp") { t.Errorf("expected sta myFunc_temp to be removed, got:\n%s", joined) } } func TestPassRegDead_KeptStoreWithLoad(t *testing.T) { input := lines( "\tlda #$05", "\tsta myFunc_temp", "\tlda myFunc_temp", "\tsta $d020", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "myFunc_temp": true, }, } output, dissolved := Optimize(input, cfg) if dissolved["myFunc_temp"] { t.Error("expected myFunc_temp NOT to be dissolved") } joined := strings.Join(output, "\n") if !strings.Contains(joined, "myFunc_temp") { t.Errorf("expected sta myFunc_temp to be kept, got:\n%s", joined) } } func TestPassRegDead_KeptStoreAtLabel(t *testing.T) { // sta regVar before a label — no lda regVar anywhere → globally dead input := lines( "\tlda #$05", "\tsta myFunc_temp", "myskip:", "\tnop", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "myFunc_temp": true, }, } 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 removed (globally dead), got:\n%s", joined) } } func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) { input := lines( "\tlda #$05", "\tsta normalVar", "\tsta $d020", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{}, } output, _ := Optimize(input, cfg) joined := strings.Join(output, "\n") if !strings.Contains(joined, "sta normalVar") { t.Errorf("expected sta normalVar to be kept (not a register var), got:\n%s", joined) } } func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) { input := lines( "\tlda #$05", "\tsta myFunc_temp", "\tsta $d020", "\trts", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "myFunc_temp": true, }, } output, dissolved := Optimize(input, cfg) if !dissolved["myFunc_temp"] { t.Error("expected myFunc_temp to be dissolved") } joined := strings.Join(output, "\n") if strings.Contains(joined, "sta myFunc_temp") { t.Errorf("expected sta myFunc_temp to be removed (dead before rts), got:\n%s", joined) } } func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) { // sta regVar; lda other → A clobbered, but regVar is reloaded later → keep store input := lines( "\tlda 53281", "\tsta spill_me_bg", "\tlda 53282", "\tsta spill_me_mc1", "\tlda spill_me_bg", "\tsta 53282", "\trts", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "spill_me_bg": true, "spill_me_mc1": true, }, } output, dissolved := Optimize(input, cfg) if dissolved["spill_me_bg"] { t.Error("spill_me_bg should NOT be dissolved — its value is reloaded") } joined := strings.Join(output, "\n") if !strings.Contains(joined, "sta spill_me_bg") { t.Errorf("expected sta spill_me_bg to be kept (reloaded after A clobber), got:\n%s", joined) } } func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) { // sta → and (A clobbered) → POKE (uses A directly) → rts. Store dead. input := lines( "\tlda 56576", "\tsta dissolve_me_temp", "\tand #$fc", "\tsta dissolve_me_temp", "\tsta 56576", "\trts", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "dissolve_me_temp": true, }, } output, dissolved := Optimize(input, cfg) if !dissolved["dissolve_me_temp"] { t.Error("dissolve_me_temp should be dissolved — never reloaded after either store") } joined := strings.Join(output, "\n") if strings.Contains(joined, "sta dissolve_me_temp") { t.Errorf("expected all sta dissolve_me_temp to be removed, got:\n%s", joined) } } 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 := lines( "\tlda 53281", "\tsta call_val", "\tjsr helper", "\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 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 := lines( "\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 — 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 (jsr before reload), got:\n%s", joined) } } func TestPassRegDead_InitValueFlowsThrough(t *testing.T) { // BYTE REGISTER x = 42; POKE $d020, x → value flows through A, never touches RAM input := lines( "\tlda #$2a", "\tsta test_x", "\tlda test_x", "\tsta 53280", "\trts", ) cfg := &Config{ EnableRegisterVars: true, EnableStoreLoad: true, RegisterVars: map[string]bool{ "test_x": true, }, } output, dissolved := Optimize(input, cfg) if !dissolved["test_x"] { t.Error("test_x should be dissolved — init value flows through A to POKE") } joined := strings.Join(output, "\n") if strings.Contains(joined, "test_x") { t.Errorf("expected no reference to test_x in output, got:\n%s", joined) } if !strings.Contains(joined, "lda #$2a") { 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 := lines( "_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 := lines( "\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) } } func TestPassRegDead_ReadModifyWrite(t *testing.T) { tests := []struct { name string opcode string lines []SourceLine }{ { name: "dec reads stored value", opcode: "dec", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tdec rmw_var", "\trts", ), }, { name: "inc reads stored value", opcode: "inc", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tinc rmw_var", "\trts", ), }, { name: "adc reads stored value", opcode: "adc", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tclc", "\tadc rmw_var", "\trts", ), }, { name: "sbc reads stored value", opcode: "sbc", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tsec", "\tsbc rmw_var", "\trts", ), }, { name: "and reads stored value", opcode: "and", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tand rmw_var", "\trts", ), }, { name: "ora reads stored value", opcode: "ora", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tora rmw_var", "\trts", ), }, { name: "eor reads stored value", opcode: "eor", lines: lines( "\tlda #$05", "\tsta rmw_var", "\teor rmw_var", "\trts", ), }, { name: "cmp reads stored value", opcode: "cmp", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tcmp rmw_var", "\trts", ), }, { name: "ldx reads stored value", opcode: "ldx", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tldx rmw_var", "\trts", ), }, { name: "ldy reads stored value", opcode: "ldy", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tldy rmw_var", "\trts", ), }, { name: "asl reads stored value", opcode: "asl", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tasl rmw_var", "\trts", ), }, { name: "lsr reads stored value", opcode: "lsr", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tlsr rmw_var", "\trts", ), }, { name: "rol reads stored value", opcode: "rol", lines: lines( "\tlda #$05", "\tsta rmw_var", "\trol rmw_var", "\trts", ), }, { name: "ror reads stored value", opcode: "ror", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tror rmw_var", "\trts", ), }, { name: "bit reads stored value", opcode: "bit", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tbit rmw_var", "\trts", ), }, { name: "cpx reads stored value", opcode: "cpx", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tcpx rmw_var", "\trts", ), }, { name: "cpy reads stored value", opcode: "cpy", lines: lines( "\tlda #$05", "\tsta rmw_var", "\tcpy rmw_var", "\trts", ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "rmw_var": true, }, } output, dissolved := Optimize(tt.lines, cfg) if dissolved["rmw_var"] { t.Errorf("rmw_var should NOT be dissolved — %s reads its value", tt.opcode) } joined := strings.Join(output, "\n") if !strings.Contains(joined, "sta rmw_var") { t.Errorf("expected sta rmw_var to be kept (followed by %s), got:\n%s", tt.opcode, joined) } }) } } func TestPassRegDead_JmpDoesNotKillStore(t *testing.T) { // sta regVar; ...(then body)...; jmp _END; _ELSE:; ...; _END:; ldy regVar // The jmp in the THEN body should NOT kill the store because // the jump target _END reaches code that reads regVar. input := lines( "\tldy #0", "\tlda (zp),y", "\tsta if_val", "\tlda test_i", "\tcmp #$06", "\tbne _I1", "\tlda (zp),y", "\tsta other", "\tjmp _I2", "_I1", "\tldy #1", "\tlda (zp),y", "\tsta other", "_I2", "\tldy if_val", "\tlda (zp),y", "\trts", ) cfg := &Config{ EnableRegisterVars: true, RegisterVars: map[string]bool{ "if_val": true, }, } output, dissolved := Optimize(input, cfg) if dissolved["if_val"] { t.Error("if_val should NOT be dissolved — ldy if_val reads its value") } joined := strings.Join(output, "\n") if !strings.Contains(joined, "sta if_val") { t.Errorf("expected sta if_val to be kept (jmp in THEN should not kill it), got:\n%s", joined) } } func TestParseLinesOrigin(t *testing.T) { t.Run("generated tab line is code", func(t *testing.T) { parsed := parseLines([]SourceLine{{Text: "\tlda x", Origin: OriginGenerated}}) if len(parsed) != 1 || !parsed[0].isCode { t.Errorf("generated tab line should be code, got %+v", parsed) } }) t.Run("generated space line is code", func(t *testing.T) { parsed := parseLines([]SourceLine{{Text: " lda x", Origin: OriginGenerated}}) if len(parsed) != 1 || !parsed[0].isCode { t.Errorf("generated space-indented line should be code, got %+v", parsed) } }) t.Run("verbatim ASM tab line is a barrier, not code", func(t *testing.T) { parsed := parseLines([]SourceLine{{Text: "\tlda x", Origin: OriginAsm}}) if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel { t.Errorf("verbatim ASM line should be a barrier, got %+v", parsed) } }) t.Run("verbatim SCRIPT tab line is a barrier", func(t *testing.T) { parsed := parseLines([]SourceLine{{Text: "\tsta $d020", Origin: OriginScript}}) if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel { t.Errorf("verbatim SCRIPT line should be a barrier, got %+v", parsed) } }) t.Run("verbatim ASM comment stays a comment", func(t *testing.T) { parsed := parseLines([]SourceLine{{Text: "; note", Origin: OriginAsm}}) if len(parsed) != 1 || !parsed[0].isComment { t.Errorf("verbatim ASM comment should be a comment, got %+v", parsed) } }) t.Run("verbatim ASM label stays a barrier", func(t *testing.T) { parsed := parseLines([]SourceLine{{Text: "h_dispatch:", Origin: OriginAsm}}) if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel { t.Errorf("verbatim ASM label should be a barrier, got %+v", parsed) } }) } func TestOptimizeSkipsVerbatimTransfer(t *testing.T) { // A tab-indented ASM block line must NOT be rewritten, even though it // looks like generated code: provenance trumps indentation. input := []SourceLine{ {Text: "\tsta x", Origin: OriginAsm}, {Text: "\tldy x", Origin: OriginAsm}, } cfg := &Config{EnableStoreLoad: true} output, _ := Optimize(input, cfg) if len(output) != 2 || output[0] != "\tsta x" || output[1] != "\tldy x" { t.Errorf("verbatim ASM lines must not be optimized, got %v", output) } } func TestPassRegDead_OriginAffectsReadDetection(t *testing.T) { regVars := map[string]bool{"bg": true} t.Run("generated read keeps store", func(t *testing.T) { input := []SourceLine{ {Text: "\tlda 53281", Origin: OriginGenerated}, {Text: "\tsta bg", Origin: OriginGenerated}, {Text: "\tlda bg", Origin: OriginGenerated}, {Text: "\tsta 53280", Origin: OriginGenerated}, {Text: "\trts", Origin: OriginGenerated}, } out, dissolved := Optimize(input, &Config{EnableRegisterVars: true, RegisterVars: regVars}) if dissolved["bg"] { t.Error("bg should NOT dissolve: a generated read exists") } if !strings.Contains(strings.Join(out, "\n"), "sta bg") { t.Errorf("expected sta bg kept (generated read), got:\n%s", strings.Join(out, "\n")) } }) t.Run("verbatim ASM read does not keep store", func(t *testing.T) { input := []SourceLine{ {Text: "\tlda 53281", Origin: OriginGenerated}, {Text: "\tsta bg", Origin: OriginGenerated}, {Text: "\tlda bg", Origin: OriginAsm}, {Text: "\tsta 53280", Origin: OriginGenerated}, {Text: "\trts", Origin: OriginGenerated}, } out, _ := Optimize(input, &Config{EnableRegisterVars: true, RegisterVars: regVars}) joined := strings.Join(out, "\n") if strings.Contains(joined, "sta bg") { t.Errorf("expected sta bg removed (verbatim ASM read is not a generated read), got:\n%s", joined) } // The verbatim ASM line itself must be preserved untouched. if !strings.Contains(joined, "\tlda bg") { t.Errorf("expected verbatim lda bg preserved, got:\n%s", joined) } }) }