peephole_optimizer #5

Merged
mattiashz merged 4 commits from peephole_optimizer into main 2026-06-04 01:26:11 +02:00
51 changed files with 941 additions and 10 deletions
Showing only changes of commit 037d232c55 - Show all commits

View file

@ -231,3 +231,6 @@ func (c *AddCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return asm, nil
}
func (c *AddCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *AddCommand) GetName() string { return "ADD" }

View file

@ -229,3 +229,6 @@ func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return asm, nil
}
func (c *AndCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *AndCommand) GetName() string { return "AND" }

View file

@ -49,3 +49,6 @@ func (c *BreakCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
fmt.Sprintf("\tjmp %s", c.skipLabel),
}, nil
}
func (c *BreakCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *BreakCommand) GetName() string { return "BREAK" }

View file

@ -149,3 +149,6 @@ func (c *ByteCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
// Variables are rendered by assembleOutput, not by individual commands
return nil, nil
}
func (c *ByteCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *ByteCommand) GetName() string { return "BYTE" }

View file

@ -75,3 +75,6 @@ func (c *CallCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
func (c *CallCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return c.asmOutput, nil
}
func (c *CallCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *CallCommand) GetName() string { return "CALL" }

View file

@ -148,3 +148,6 @@ func (c *CaseCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *CaseCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *CaseCommand) GetName() string { return "CASE" }

View file

@ -139,3 +139,6 @@ func (c *DecrCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *DecrCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *DecrCommand) GetName() string { return "DEC" }

View file

@ -76,3 +76,6 @@ func (c *DefaultCommand) Generate(ctx *compiler.CompilerContext) ([]string, erro
return asm, nil
}
func (c *DefaultCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *DefaultCommand) GetName() string { return "DEFAULT" }

View file

@ -54,3 +54,6 @@ func (c *ElseCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
c.skipLabel,
}, nil
}
func (c *ElseCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *ElseCommand) GetName() string { return "ELSE" }

View file

@ -47,3 +47,6 @@ func (c *EndIfCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContex
func (c *EndIfCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return []string{c.endLabel}, nil
}
func (c *EndIfCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *EndIfCommand) GetName() string { return "ENDIF" }

View file

@ -58,3 +58,6 @@ func (c *EndSwitchCommand) Generate(ctx *compiler.CompilerContext) ([]string, er
return asm, nil
}
func (c *EndSwitchCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *EndSwitchCommand) GetName() string { return "ENDSWITCH" }

View file

@ -47,3 +47,6 @@ func (c *FendCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
}
return lines, nil
}
func (c *FendCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
func (c *FendCommand) GetName() string { return "FEND" }

View file

@ -243,3 +243,6 @@ func (c *ForCommand) generateAssignment() []string {
return asm
}
func (c *ForCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *ForCommand) GetName() string { return "FOR" }

View file

@ -48,3 +48,6 @@ func (c *FuncCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
marker := fmt.Sprintf("; @@FUNC_BEGIN %s", c.funcName)
return []string{marker, c.funcName}, nil
}
func (c *FuncCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
func (c *FuncCommand) GetName() string { return "FUNC" }

View file

@ -222,6 +222,9 @@ func (c *GosubCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *GosubCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *GosubCommand) GetName() string { return "GOSUB" }
func (c *GosubCommand) generatePrePassing() []string {
var asm []string

View file

@ -98,3 +98,6 @@ func (c *GotoCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
// Jump to computed address
return []string{fmt.Sprintf("\tjmp $%04x", c.address)}, nil
}
func (c *GotoCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *GotoCommand) GetName() string { return "GOTO" }

View file

@ -119,3 +119,6 @@ func (c *IfCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
return cmpAsm, nil
}
func (c *IfCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *IfCommand) GetName() string { return "IF" }

View file

@ -138,3 +138,6 @@ func (c *IncrCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *IncrCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *IncrCommand) GetName() string { return "INC" }

View file

@ -46,3 +46,6 @@ func (c *LabelCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext)
func (c *LabelCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return []string{c.labelName}, nil
}
func (c *LabelCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
func (c *LabelCommand) GetName() string { return "LABEL" }

View file

@ -182,3 +182,6 @@ func (c *LetCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return asm, nil
}
func (c *LetCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *LetCommand) GetName() string { return "LET" }

View file

@ -44,3 +44,6 @@ func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
result = append(result, fmt.Sprintf("; end @%s", c.macroName))
return result, nil
}
func (c *MacroCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
func (c *MacroCommand) GetName() string { return "MACRO" }

View file

@ -74,6 +74,9 @@ func (c *NextCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *NextCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *NextCommand) GetName() string { return "NEXT" }
// generateEndCheck generates code to exit if var == end.
func (c *NextCommand) generateEndCheck(ctx *compiler.CompilerContext) []string {
var asm []string

View file

@ -229,3 +229,6 @@ func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return asm, nil
}
func (c *OrCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *OrCommand) GetName() string { return "OR" }

View file

@ -54,3 +54,6 @@ func (c *OriginCommand) Interpret(line preproc.Line, ctx *compiler.CompilerConte
func (c *OriginCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return []string{fmt.Sprintf("*= $%04x", c.address)}, nil
}
func (c *OriginCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *OriginCommand) GetName() string { return "ORIGIN" }

View file

@ -280,6 +280,17 @@ func (c *PeekCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *PeekCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *PeekCommand) GetName() string {
if c.isZPPointer {
return "PEEK:ZP"
}
if c.isAddrVar {
return "PEEK:SM"
}
return "PEEK"
}
// parseOffset extracts offset from syntax like "var[offset]"
// Returns (base, offset) where offset is empty if no brackets found
func parseOffset(param string) (base string, offset string) {

View file

@ -281,6 +281,17 @@ func (c *PeekWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *PeekWCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *PeekWCommand) GetName() string {
if c.isZPPointer {
return "PEEKW:ZP"
}
if c.isAddrVar {
return "PEEKW:SM"
}
return "PEEKW"
}
// parseOffsetW extracts offset from syntax like "var[offset]"
// Returns (base, offset) where offset is empty if no brackets found
func parseOffsetW(param string) (base string, offset string) {

View file

@ -132,3 +132,6 @@ func (c *PointerCommand) Generate(ctx *compiler.CompilerContext) ([]string, erro
return asm, nil
}
func (c *PointerCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *PointerCommand) GetName() string { return "POINT" }

View file

@ -258,6 +258,17 @@ func (c *PokeCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *PokeCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *PokeCommand) GetName() string {
if c.isZPPointer {
return "POKE:ZP"
}
if c.isAddrVar {
return "POKE:SM"
}
return "POKE"
}
// parsePOKEOffset extracts offset from syntax like "var[offset]"
// Returns (base, offset) where offset is empty if no brackets found
func parsePOKEOffset(param string) (base string, offset string) {

View file

@ -294,6 +294,17 @@ func (c *PokeWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
return asm, nil
}
func (c *PokeWCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *PokeWCommand) GetName() string {
if c.isZPPointer {
return "POKEW:ZP"
}
if c.isAddrVar {
return "POKEW:SM"
}
return "POKEW"
}
// parsePOKEWOffset extracts offset from syntax like "var[offset]"
// Returns (base, offset) where offset is empty if no brackets found
func parsePOKEWOffset(param string) (base string, offset string) {

View file

@ -510,3 +510,6 @@ func (c *ShiftLCommand) generateWordShiftVar(ctx *compiler.CompilerContext) ([]s
return asm, nil
}
func (c *ShiftLCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *ShiftLCommand) GetName() string { return "SHL" }

View file

@ -653,3 +653,5 @@ func (c *ShiftRCommand) generateWordShiftVar(ctx *compiler.CompilerContext) ([]s
return asm, nil
}
func (c *ShiftRCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *ShiftRCommand) GetName() string { return "SHR" }

View file

@ -42,3 +42,6 @@ func (c *SubEndCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext
func (c *SubEndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return []string{"\trts"}, nil
}
func (c *SubEndCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *SubEndCommand) GetName() string { return "SUBEND" }

View file

@ -251,3 +251,6 @@ func (c *SubtractCommand) Generate(_ *compiler.CompilerContext) ([]string, error
return asm, nil
}
func (c *SubtractCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *SubtractCommand) GetName() string { return "SUB" }

View file

@ -74,3 +74,6 @@ func (c *SwitchCommand) Generate(ctx *compiler.CompilerContext) ([]string, error
// The variable will be loaded and compared by each CASE
return []string{}, nil
}
func (c *SwitchCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *SwitchCommand) GetName() string { return "SWITCH" }

View file

@ -57,3 +57,6 @@ func (c *WendCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
c.skipLabel,
}, nil
}
func (c *WendCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *WendCommand) GetName() string { return "WEND" }

View file

@ -125,3 +125,6 @@ func (c *WhileCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
asm = append(asm, cmpAsm...)
return asm, nil
}
func (c *WhileCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
func (c *WhileCommand) GetName() string { return "WHILE" }

View file

@ -204,3 +204,6 @@ func (c *WordCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
// Variables are rendered by assembleOutput, not by individual commands
return nil, nil
}
func (c *WordCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *WordCommand) GetName() string { return "WORD" }

View file

@ -267,3 +267,6 @@ func (c *XorCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return asm, nil
}
func (c *XorCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
func (c *XorCommand) GetName() string { return "XOR" }

View file

@ -7,6 +7,15 @@ import (
"c65gm/internal/preproc"
)
// CmdClass categorizes commands for the peephole optimizer
type CmdClass int
const (
CmdLinear CmdClass = iota // straight-line code, no visible labels
CmdFlowCtrl // IF/WHILE/FOR/SWITCH: internal labels, bodies still optimizable
CmdBoundary // FUNC/FEND/LABEL/MACRO: reset all optimizer state
)
// Command represents a script command that can interpret source lines and generate assembly
type Command interface {
// WillHandle checks if this command can handle the given line
@ -20,6 +29,12 @@ type Command interface {
// Generate produces assembly output based on previously interpreted line
// Returns assembly lines and error if generation fails
Generate(ctx *CompilerContext) ([]string, error)
// GetClass returns the command's optimizer class
GetClass() CmdClass
// GetName returns a short command name for @@OPT annotations (e.g. "LET", "ADD", "POKE")
GetName() string
}
// CommandRegistry manages registered commands and dispatches lines to appropriate handlers

View file

@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"c65gm/internal/optimizer"
"c65gm/internal/preproc"
)
@ -233,6 +234,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
}
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
if len(asmLines) > 0 && c.isOptimizing() {
codeOutput = append(codeOutput, fmt.Sprintf("; @@OPT:%s:%s", classString(cmd.GetClass()), cmd.GetName()))
}
codeOutput = append(codeOutput, asmLines...)
}
@ -251,6 +255,11 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
return nil, fmt.Errorf("Unclosed SCRIPT MACRO block.")
}
// Peephole optimization pass
if cfg := c.getOptimizerConfig(); cfg != nil {
codeOutput = optimizer.Optimize(codeOutput, cfg)
}
// Analyze for overlapping absolute addresses in function call chains
c.checkAbsoluteOverlaps()
@ -276,6 +285,44 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
return c.assembleOutput(codeOutput, removedFuncs), nil
}
// isOptimizing returns true if any peephole optimization pragma is active
func (c *Compiler) isOptimizing() bool {
if c.ctx == nil || c.ctx.Pragma == nil {
return false
}
idx := c.ctx.Pragma.GetCurrentPragmaSetIndex()
ps := c.ctx.Pragma.GetPragmaSetByIndex(idx)
cfg := optimizer.NewConfig(ps)
return cfg.Any()
}
// getOptimizerConfig returns optimizer config if optimization is enabled, nil otherwise
func (c *Compiler) getOptimizerConfig() *optimizer.Config {
if c.ctx == nil || c.ctx.Pragma == nil {
return nil
}
idx := c.ctx.Pragma.GetCurrentPragmaSetIndex()
ps := c.ctx.Pragma.GetPragmaSetByIndex(idx)
cfg := optimizer.NewConfig(ps)
if !cfg.Any() {
return nil
}
return cfg
}
func classString(cls CmdClass) string {
switch cls {
case CmdLinear:
return "LINEAR"
case CmdFlowCtrl:
return "FLOWCTRL"
case CmdBoundary:
return "BOUNDARY"
default:
return "UNKNOWN"
}
}
// printErrorWithContext prints an error with source code context to stderr
func (c *Compiler) printErrorWithContext(lines []preproc.Line, lineIndex int, err error) {
if lineIndex < 0 || lineIndex >= len(lines) {

View file

@ -49,6 +49,9 @@ func (c *TestBreakCommand) Generate(ctx *CompilerContext) ([]string, error) {
}, nil
}
func (c *TestBreakCommand) GetClass() CmdClass { return CmdLinear }
func (c *TestBreakCommand) GetName() string { return "BREAK" }
func TestCompilerArchitecture(t *testing.T) {
// Create pragma
pragma := preproc.NewPragma()

View file

@ -0,0 +1,29 @@
package optimizer
import (
"c65gm/internal/preproc"
)
type Config struct {
EnableLoad bool
EnableImm bool
EnableJmp bool
EnableSelf bool
Debug 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"),
}
}
func (c *Config) Any() bool {
return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf
}

View file

@ -0,0 +1,11 @@
package optimizer
import "fmt"
// debugWithOriginals wraps the current state with a debug comment header
func debugWithOriginals(lines []asmLine, phase string) []asmLine {
header := fmt.Sprintf("; --- %s ---", phase)
result := []asmLine{{text: header, isComment: true}}
result = append(result, lines...)
return result
}

View file

@ -0,0 +1,36 @@
package optimizer
// Optimize applies all enabled peephole passes to the generated ASM lines.
// Each pass runs sequentially on a parsed representation of the lines.
// @@OPT markers are stripped from output.
func Optimize(lines []string, cfg *Config) []string {
if cfg == nil || !cfg.Any() {
return lines
}
parsed := parseLines(lines)
if cfg.Debug {
parsed = debugWithOriginals(parsed, "before peephole")
}
if cfg.EnableLoad {
parsed = passLoadElimination(parsed)
}
if cfg.EnableImm {
parsed = passImmElimination(parsed)
}
if cfg.EnableJmp {
parsed = passJmpNext(parsed)
}
if cfg.EnableSelf {
parsed = passSelfAssignment(parsed)
}
if cfg.Debug {
parsed = debugWithOriginals(parsed, "after peephole")
}
parsed = stripOptMarkers(parsed)
return linesToString(parsed)
}

View file

@ -0,0 +1,209 @@
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)
}
}

View file

@ -0,0 +1,67 @@
package optimizer
// passImmElimination removes redundant immediate loads.
// Pattern: lda #N; ... (no A modification) ...; lda #N → remove second
// Applies to A, X, Y.
func passImmElimination(lines []asmLine) []asmLine {
rs := newRegState()
var result []asmLine
for _, line := range lines {
if !line.isCode {
if line.optMarker {
continue
}
if line.isLabel {
rs.reset()
}
result = append(result, line)
continue
}
if isChainBreaker(line.opcode) {
rs.reset()
result = append(result, line)
continue
}
if isRedundantImm(line, rs) {
continue // remove line
}
updateRegState(line, rs)
result = append(result, line)
}
return result
}
func isRedundantImm(line asmLine, rs *regState) bool {
switch line.opcode {
case "lda":
if isImmediate(line.operand) {
val := parseHexOrDec(line.operand[1:])
if rs.a.val == val {
return true
}
rs.loadImm("a", val)
}
case "ldx":
if isImmediate(line.operand) {
val := parseHexOrDec(line.operand[1:])
if rs.x.val == val {
return true
}
rs.loadImm("x", val)
}
case "ldy":
if isImmediate(line.operand) {
val := parseHexOrDec(line.operand[1:])
if rs.y.val == val {
return true
}
rs.loadImm("y", val)
}
}
return false
}

View file

@ -0,0 +1,38 @@
package optimizer
import "strings"
// passJmpNext removes unconditional jumps to the immediately following label.
// Pattern: jmp L; L: → (remove jmp)
func passJmpNext(lines []asmLine) []asmLine {
var result []asmLine
skipNext := false
for i := 0; i < len(lines); i++ {
if skipNext {
skipNext = false
continue
}
line := lines[i]
if skipJmpMarker(line) {
continue
}
// Check for jmp L followed by label L:
if line.isCode && line.opcode == "jmp" && i+1 < len(lines) {
next := lines[i+1]
if next.isLabel && strings.TrimSpace(next.text) == strings.TrimSpace(line.operand) {
// Remove the jmp, keep the label
result = append(result, next)
skipNext = true
continue
}
}
result = append(result, line)
}
return result
}

View file

@ -0,0 +1,56 @@
package optimizer
// passLoadElimination removes redundant memory-to-register loads.
// Pattern: lda X; ... (X not modified) ...; lda X → remove second
// Applies to A, X, Y registers identically.
func passLoadElimination(lines []asmLine) []asmLine {
rs := newRegState()
var result []asmLine
for _, line := range lines {
if !line.isCode {
if line.optMarker {
continue
}
if line.isLabel {
rs.reset()
}
result = append(result, line)
continue
}
if isChainBreaker(line.opcode) {
rs.reset()
result = append(result, line)
continue
}
// Check for redundant memory load
if line.opcode == "lda" && !isImmediate(line.operand) {
if rs.a.src == line.operand {
continue // redundant, remove
}
rs.a.src = line.operand
} else if line.opcode == "ldx" && !isImmediate(line.operand) {
if rs.x.src == line.operand {
continue
}
rs.x.src = line.operand
} else if line.opcode == "ldy" && !isImmediate(line.operand) {
if rs.y.src == line.operand {
continue
}
rs.y.src = line.operand
} else {
updateRegState(line, rs)
}
result = append(result, line)
}
return result
}
func isImmediate(operand string) bool {
return len(operand) > 0 && operand[0] == '#'
}

View file

@ -0,0 +1,38 @@
package optimizer
import "strings"
// passSelfAssignment removes lda X; sta X pairs where the store immediately
// follows the load and neither line is a branch target.
func passSelfAssignment(lines []asmLine) []asmLine {
var result []asmLine
skipNext := false
for i := 0; i < len(lines); i++ {
if skipNext {
skipNext = false
continue
}
line := lines[i]
if skipJmpMarker(line) {
continue
}
// Check for lda X; sta X
if line.isCode && line.opcode == "lda" && !isImmediate(line.operand) && i+1 < len(lines) {
next := lines[i+1]
if next.isCode && next.opcode == "sta" && next.operand == line.operand {
if !strings.Contains(line.operand, "+") {
skipNext = true // skip sta on next iteration
continue // skip lda
}
}
}
result = append(result, line)
}
return result
}

View file

@ -0,0 +1,140 @@
package optimizer
import "strings"
type regInfo struct {
val int // -1 = unknown, else the immediate value known to be in reg
src string // variable name if loaded from memory (or "" for immediates)
}
type regState struct {
a regInfo
x regInfo
y regInfo
}
func newRegState() *regState {
return &regState{
a: regInfo{val: -1},
x: regInfo{val: -1},
y: regInfo{val: -1},
}
}
func (rs *regState) reset() {
rs.a = regInfo{val: -1}
rs.x = regInfo{val: -1}
rs.y = regInfo{val: -1}
}
func (rs *regState) resetA() { rs.a = regInfo{val: -1} }
func (rs *regState) resetX() { rs.x = regInfo{val: -1} }
func (rs *regState) resetY() { rs.y = regInfo{val: -1} }
// loadImm records that a register was loaded with an immediate value
func (rs *regState) loadImm(reg string, val int) {
r := regInfo{val: val}
switch reg {
case "a":
rs.a = r
case "x":
rs.x = r
case "y":
rs.y = r
}
}
// loadMem records that a register was loaded from a memory location
func (rs *regState) loadMem(reg, varName string) {
r := regInfo{val: -1, src: varName}
switch reg {
case "a":
rs.a = r
case "x":
rs.x = r
case "y":
rs.y = r
}
}
// storeInvalidate marks that a memory location was written.
// If any register's src matches, that register becomes "unknown" (value gone).
func (rs *regState) storeInvalidate(varName string) {
if rs.a.src == varName {
rs.a.src = ""
rs.a.val = -1
}
if rs.x.src == varName {
rs.x.src = ""
rs.x.val = -1
}
if rs.y.src == varName {
rs.y.src = ""
rs.y.val = -1
}
}
// isChainBreaker returns true for instructions that reset all register tracking
func isChainBreaker(opcode string) bool {
switch opcode {
case "jmp", "jsr", "rts", "rti", "brk":
return true
}
return false
}
// updateRegState updates the register state machine for a code line
func updateRegState(line asmLine, rs *regState) {
if !line.isCode {
return
}
switch line.opcode {
case "lda":
if strings.HasPrefix(line.operand, "#") {
rs.loadImm("a", parseHexOrDec(line.operand[1:]))
} else {
rs.loadMem("a", line.operand)
}
case "ldx":
if strings.HasPrefix(line.operand, "#") {
rs.loadImm("x", parseHexOrDec(line.operand[1:]))
} else {
rs.loadMem("x", line.operand)
}
case "ldy":
if strings.HasPrefix(line.operand, "#") {
rs.loadImm("y", parseHexOrDec(line.operand[1:]))
} else {
rs.loadMem("y", line.operand)
}
case "sta":
rs.storeInvalidate(line.operand)
case "stx":
rs.storeInvalidate(line.operand)
case "sty":
rs.storeInvalidate(line.operand)
case "inc", "dec":
rs.storeInvalidate(line.operand)
case "tax":
rs.x = regInfo{val: -1, src: rs.a.src}
case "tay":
rs.y = regInfo{val: -1, src: rs.a.src}
case "txa":
rs.a = regInfo{val: -1, src: rs.x.src}
case "tya":
rs.a = regInfo{val: -1, src: rs.y.src}
case "inx", "dex":
rs.resetX()
case "iny", "dey":
rs.resetY()
case "asl", "lsr", "rol", "ror", "adc", "sbc", "and", "ora", "eor", "pla":
rs.resetA()
case "plp":
rs.reset()
case "tsx":
rs.resetX()
case "txs":
// SP doesn't affect register tracking
}
}

View file

@ -0,0 +1,97 @@
package optimizer
import (
"strconv"
"strings"
)
// parseHexOrDec parses an ACME-style number: $ff or 255
func parseHexOrDec(s string) int {
if strings.HasPrefix(s, "$") {
v, _ := strconv.ParseInt(s[1:], 16, 16)
return int(v)
}
// Strip trailing characters after the number (e.g., ",y" in "5,y")
if idx := strings.IndexAny(s, ",)"); idx >= 0 {
s = s[:idx]
}
v, _ := strconv.ParseInt(s, 10, 16)
return int(v)
}
type asmLine struct {
text string
isLabel bool
isCode bool
isComment bool
optMarker bool
opcode string
operand string
}
func parseLines(lines []string) []asmLine {
var result []asmLine
for _, l := range lines {
al := asmLine{text: l}
if l == "" {
result = append(result, al)
continue
}
if strings.HasPrefix(l, "; @@OPT:") {
al.optMarker = true
al.isComment = true
result = append(result, al)
continue
}
if strings.HasPrefix(l, ";") {
al.isComment = true
result = append(result, al)
continue
}
if l[0] == '\t' {
al.isCode = true
parts := strings.Fields(l)
if len(parts) > 0 {
al.opcode = strings.ToLower(parts[0])
}
if len(parts) > 1 {
al.operand = parts[1]
}
} else {
al.isLabel = true
}
result = append(result, al)
}
return result
}
// stripOptMarkers removes @@OPT comment lines from the output
func stripOptMarkers(lines []asmLine) []asmLine {
var result []asmLine
for _, l := range lines {
if l.optMarker {
continue
}
result = append(result, l)
}
return result
}
func linesToString(lines []asmLine) []string {
var result []string
for _, l := range lines {
result = append(result, l.text)
}
return result
}
// skipJmpMarker returns true if the line is an @@OPT comment (to be skipped in pass logic)
func skipJmpMarker(line asmLine) bool {
return line.optMarker
}