518 lines
14 KiB
Go
518 lines
14 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, 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 []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 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("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 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 []string
|
|
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, but IOMap uses hex lookup
|
|
// The pass checks $ prefix only, decimal addresses won't be caught by IOMap
|
|
parsed := parseLines(lines("\tsta 53280", "\tlda 53280"))
|
|
result := passStoreReload(parsed, cfg)
|
|
cleaned := stripOptMarkers(result)
|
|
if len(cleaned) != 1 {
|
|
t.Errorf("expected 1 line (decimal not caught by I/O), 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))
|
|
}
|
|
}
|
|
})
|
|
}
|