c65gm/internal/optimizer/optimizer_test.go

252 lines
6.3 KiB
Go

package optimizer
import (
"testing"
)
func lines(s ...string) []string { return s }
func TestPassLoadElimination(t *testing.T) {
tests := []struct {
name string
input []string
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)
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 []string
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 []string
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 []string
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 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)
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 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
}