From 28723878b11db880ea769aa6dc6565cf5c24d147 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Thu, 4 Jun 2026 00:05:24 +0200 Subject: [PATCH] Optimizer removed redundant lda's --- internal/compiler/compiler.go | 1 + internal/optimizer/config.go | 67 +++++++++--- internal/optimizer/optimizer.go | 3 + internal/optimizer/optimizer_test.go | 149 +++++++++++++++++++++++++++ internal/optimizer/pass_stld.go | 91 ++++++++++++++++ internal/optimizer/scanner.go | 4 +- 6 files changed, 300 insertions(+), 15 deletions(-) create mode 100644 internal/optimizer/pass_stld.go diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 40efdc2..dc5e14c 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -317,6 +317,7 @@ func (c *Compiler) getOptimizerConfig() *optimizer.Config { if !cfg.Any() { return nil } + cfg.BuildIOMap(c.ctx.Pragma) return cfg } diff --git a/internal/optimizer/config.go b/internal/optimizer/config.go index a6d7ef0..7fd6ff2 100644 --- a/internal/optimizer/config.go +++ b/internal/optimizer/config.go @@ -1,31 +1,72 @@ package optimizer import ( + "strconv" + "strings" + "c65gm/internal/preproc" ) type Config struct { - EnableLoad bool - EnableImm bool - EnableJmp bool - EnableSelf bool - Debug bool - ShowMarkers bool + EnableLoad bool + EnableImm bool + EnableJmp bool + EnableSelf bool + EnableStoreLoad bool + Debug bool + ShowMarkers bool + IOMap [65536]bool } func NewConfig(ps preproc.PragmaSet) *Config { all := ps.GetPragma("_P_OPT_ALL") != "" && ps.GetPragma("_P_OPT_ALL") != "0" return &Config{ - EnableLoad: all || (ps.GetPragma("_P_OPT_LOAD") != "" && ps.GetPragma("_P_OPT_LOAD") != "0"), - EnableImm: all || (ps.GetPragma("_P_OPT_IMM") != "" && ps.GetPragma("_P_OPT_IMM") != "0"), - EnableJmp: all || (ps.GetPragma("_P_OPT_JMP") != "" && ps.GetPragma("_P_OPT_JMP") != "0"), - EnableSelf: all || (ps.GetPragma("_P_OPT_SELF") != "" && ps.GetPragma("_P_OPT_SELF") != "0"), - Debug: (ps.GetPragma("_P_OPT_DEBUG") != "" && ps.GetPragma("_P_OPT_DEBUG") != "0"), - ShowMarkers: (ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"), + EnableLoad: all || (ps.GetPragma("_P_OPT_LOAD") != "" && ps.GetPragma("_P_OPT_LOAD") != "0"), + EnableImm: all || (ps.GetPragma("_P_OPT_IMM") != "" && ps.GetPragma("_P_OPT_IMM") != "0"), + EnableJmp: all || (ps.GetPragma("_P_OPT_JMP") != "" && ps.GetPragma("_P_OPT_JMP") != "0"), + EnableSelf: all || (ps.GetPragma("_P_OPT_SELF") != "" && ps.GetPragma("_P_OPT_SELF") != "0"), + EnableStoreLoad: all || (ps.GetPragma("_P_OPT_STLD") != "" && ps.GetPragma("_P_OPT_STLD") != "0"), + Debug: (ps.GetPragma("_P_OPT_DEBUG") != "" && ps.GetPragma("_P_OPT_DEBUG") != "0"), + ShowMarkers: (ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"), } } func (c *Config) Any() bool { - return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf + return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf || c.EnableStoreLoad +} + +// BuildIOMap scans all pragma sets for _P_OPT_IO and marks I/O regions. +func (c *Config) BuildIOMap(pragma *preproc.Pragma) { + for i := 0; i <= pragma.GetCurrentPragmaSetIndex(); i++ { + val := pragma.GetPragmaSetByIndex(i).GetPragma("_P_OPT_IO") + if val == "" { + continue + } + parts := strings.Fields(val) + if len(parts) >= 2 { + start := parsePragmaAddr(parts[0]) + end := parsePragmaAddr(parts[1]) + if start >= 0 && end >= 0 && start <= end && start < 65536 { + for a := start; a <= end && a < 65536; a++ { + c.IOMap[a] = true + } + } + } + } +} + +func parsePragmaAddr(s string) int { + if strings.HasPrefix(s, "$") { + v, err := strconv.ParseUint(s[1:], 16, 16) + if err != nil { + return -1 + } + return int(v) + } + v, err := strconv.ParseUint(s, 10, 16) + if err != nil { + return -1 + } + return int(v) } diff --git a/internal/optimizer/optimizer.go b/internal/optimizer/optimizer.go index 770fd68..e272c0b 100644 --- a/internal/optimizer/optimizer.go +++ b/internal/optimizer/optimizer.go @@ -11,6 +11,9 @@ func Optimize(lines []string, cfg *Config) []string { parsed := parseLines(lines) original := parsed + if cfg.EnableStoreLoad { + parsed = passStoreReload(parsed, cfg) + } if cfg.EnableLoad { parsed = passLoadElimination(parsed) } diff --git a/internal/optimizer/optimizer_test.go b/internal/optimizer/optimizer_test.go index a0b35d8..3f21192 100644 --- a/internal/optimizer/optimizer_test.go +++ b/internal/optimizer/optimizer_test.go @@ -250,3 +250,152 @@ func containsSubstring(lines []string, sub string) bool { } 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) { + 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)) + } +} diff --git a/internal/optimizer/pass_stld.go b/internal/optimizer/pass_stld.go new file mode 100644 index 0000000..2871a02 --- /dev/null +++ b/internal/optimizer/pass_stld.go @@ -0,0 +1,91 @@ +package optimizer + +import "strings" + +// passStoreReload removes redundant reloads immediately after stores. +// Pattern: sta X; ...(non-A-modifying)...; lda X → remove lda +func passStoreReload(lines []asmLine, cfg *Config) []asmLine { + var result []asmLine + + for i := 0; i < len(lines); i++ { + line := lines[i] + + if line.isCode && line.opcode == "sta" && isSafeStldOperand(line.operand, cfg) { + res := findMatchingLoad(lines, i+1, line.operand, cfg) + if res.found { + result = append(result, line) + for j := i + 1; j < res.idx; j++ { + result = append(result, lines[j]) + } + i = res.idx // skip the lda on next iteration + continue + } + } + + result = append(result, line) + } + + return result +} + +type matchResult struct { + idx int + found bool +} + +// findMatchingLoad scans forward from `start` for `lda operand`. +// Only passes through comments and non-A-modifying code. +func findMatchingLoad(lines []asmLine, start int, operand string, cfg *Config) matchResult { + for i := start; i < len(lines); i++ { + l := lines[i] + + if l.optMarker || l.isComment { + continue + } + if l.isLabel { + return matchResult{} + } + if l.isCode && l.opcode == "lda" && l.operand == operand && isSafeStldOperand(l.operand, cfg) { + return matchResult{i, true} + } + if modifiesA(l) { + return matchResult{} + } + } + return matchResult{} +} + +// modifiesA returns true if the instruction puts a new value into A. +func modifiesA(line asmLine) bool { + if !line.isCode { + return false + } + switch line.opcode { + case "lda", "adc", "sbc", "and", "ora", "eor", "pla", "txa", "tya": + return true + case "asl", "lsr", "rol", "ror": + return line.operand == "" || line.operand == "a" + case "inc", "dec": + return false + default: + return false + } +} + +// isSafeStldOperand returns true if the operand is safe for store-then-reload optimization. +// Unsafe: I/O addresses (in cfg.IOMap), self-modifying code pattern (+N), indexed/indirect. +func isSafeStldOperand(operand string, cfg *Config) bool { + if strings.ContainsAny(operand, "(,+") { + return false + } + + // Check if operand is a direct hex address in an I/O region + if strings.HasPrefix(operand, "$") { + addr := parseHexOrDec(operand) + if addr >= 0 && addr < 65536 && cfg.IOMap[addr] { + return false + } + } + + return true +} diff --git a/internal/optimizer/scanner.go b/internal/optimizer/scanner.go index 6dbe879..1217ea3 100644 --- a/internal/optimizer/scanner.go +++ b/internal/optimizer/scanner.go @@ -9,7 +9,7 @@ import ( // Returns -1 if s is not a parseable simple number (e.g. label references, label). func parseHexOrDec(s string) int { if strings.HasPrefix(s, "$") { - v, err := strconv.ParseInt(s[1:], 16, 16) + v, err := strconv.ParseUint(s[1:], 16, 16) if err != nil { return -1 } @@ -19,7 +19,7 @@ func parseHexOrDec(s string) int { if idx := strings.IndexAny(s, ",)"); idx >= 0 { s = s[:idx] } - v, err := strconv.ParseInt(s, 10, 16) + v, err := strconv.ParseUint(s, 10, 16) if err != nil { return -1 }