From 037d232c558ba1db24d7df62086e4ffe29a8dbb1 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Wed, 3 Jun 2026 21:51:29 +0200 Subject: [PATCH] First naive version of optimizer --- internal/commands/add.go | 3 + internal/commands/and.go | 3 + internal/commands/break.go | 3 + internal/commands/byte.go | 3 + internal/commands/call.go | 3 + internal/commands/case.go | 3 + internal/commands/decr.go | 3 + internal/commands/default.go | 3 + internal/commands/else.go | 3 + internal/commands/endif.go | 3 + internal/commands/endswitch.go | 3 + internal/commands/fend.go | 3 + internal/commands/for.go | 3 + internal/commands/func.go | 3 + internal/commands/gosub.go | 3 + internal/commands/goto.go | 3 + internal/commands/if.go | 3 + internal/commands/incr.go | 3 + internal/commands/label.go | 3 + internal/commands/let.go | 3 + internal/commands/macro.go | 3 + internal/commands/next.go | 3 + internal/commands/or.go | 3 + internal/commands/origin.go | 3 + internal/commands/peek.go | 11 ++ internal/commands/peekw.go | 11 ++ internal/commands/point.go | 3 + internal/commands/poke.go | 11 ++ internal/commands/pokew.go | 11 ++ internal/commands/shiftl.go | 15 +- internal/commands/shiftr.go | 10 +- internal/commands/subend.go | 3 + internal/commands/subtr.go | 3 + internal/commands/switch.go | 3 + internal/commands/wend.go | 3 + internal/commands/while.go | 3 + internal/commands/word.go | 3 + internal/commands/xor.go | 3 + internal/compiler/command.go | 15 ++ internal/compiler/compiler.go | 47 ++++++ internal/compiler/compiler_test.go | 3 + internal/optimizer/config.go | 29 ++++ internal/optimizer/debug.go | 11 ++ internal/optimizer/optimizer.go | 36 +++++ internal/optimizer/optimizer_test.go | 209 +++++++++++++++++++++++++++ internal/optimizer/pass_imm.go | 67 +++++++++ internal/optimizer/pass_jmp.go | 38 +++++ internal/optimizer/pass_load.go | 56 +++++++ internal/optimizer/pass_self.go | 38 +++++ internal/optimizer/regstate.go | 140 ++++++++++++++++++ internal/optimizer/scanner.go | 97 +++++++++++++ 51 files changed, 941 insertions(+), 10 deletions(-) create mode 100644 internal/optimizer/config.go create mode 100644 internal/optimizer/debug.go create mode 100644 internal/optimizer/optimizer.go create mode 100644 internal/optimizer/optimizer_test.go create mode 100644 internal/optimizer/pass_imm.go create mode 100644 internal/optimizer/pass_jmp.go create mode 100644 internal/optimizer/pass_load.go create mode 100644 internal/optimizer/pass_self.go create mode 100644 internal/optimizer/regstate.go create mode 100644 internal/optimizer/scanner.go diff --git a/internal/commands/add.go b/internal/commands/add.go index e824435..cf27148 100644 --- a/internal/commands/add.go +++ b/internal/commands/add.go @@ -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" } diff --git a/internal/commands/and.go b/internal/commands/and.go index 6c63eab..27867f0 100644 --- a/internal/commands/and.go +++ b/internal/commands/and.go @@ -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" } diff --git a/internal/commands/break.go b/internal/commands/break.go index f5e8835..e0ee485 100644 --- a/internal/commands/break.go +++ b/internal/commands/break.go @@ -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" } diff --git a/internal/commands/byte.go b/internal/commands/byte.go index 124232e..02c6bb6 100644 --- a/internal/commands/byte.go +++ b/internal/commands/byte.go @@ -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" } diff --git a/internal/commands/call.go b/internal/commands/call.go index d3fb175..97b020c 100644 --- a/internal/commands/call.go +++ b/internal/commands/call.go @@ -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" } diff --git a/internal/commands/case.go b/internal/commands/case.go index febb002..ebc555a 100644 --- a/internal/commands/case.go +++ b/internal/commands/case.go @@ -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" } diff --git a/internal/commands/decr.go b/internal/commands/decr.go index 6c393f9..2c55718 100644 --- a/internal/commands/decr.go +++ b/internal/commands/decr.go @@ -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" } diff --git a/internal/commands/default.go b/internal/commands/default.go index 711ed78..f83f228 100644 --- a/internal/commands/default.go +++ b/internal/commands/default.go @@ -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" } diff --git a/internal/commands/else.go b/internal/commands/else.go index e920a11..cb8a623 100644 --- a/internal/commands/else.go +++ b/internal/commands/else.go @@ -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" } diff --git a/internal/commands/endif.go b/internal/commands/endif.go index b197077..c0231eb 100644 --- a/internal/commands/endif.go +++ b/internal/commands/endif.go @@ -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" } diff --git a/internal/commands/endswitch.go b/internal/commands/endswitch.go index 5265d88..b51f9b8 100644 --- a/internal/commands/endswitch.go +++ b/internal/commands/endswitch.go @@ -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" } diff --git a/internal/commands/fend.go b/internal/commands/fend.go index 9bebb47..115629a 100644 --- a/internal/commands/fend.go +++ b/internal/commands/fend.go @@ -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" } diff --git a/internal/commands/for.go b/internal/commands/for.go index 6955872..85523b1 100644 --- a/internal/commands/for.go +++ b/internal/commands/for.go @@ -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" } diff --git a/internal/commands/func.go b/internal/commands/func.go index 0733efb..7091d77 100644 --- a/internal/commands/func.go +++ b/internal/commands/func.go @@ -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" } diff --git a/internal/commands/gosub.go b/internal/commands/gosub.go index ee4833c..962f119 100644 --- a/internal/commands/gosub.go +++ b/internal/commands/gosub.go @@ -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 diff --git a/internal/commands/goto.go b/internal/commands/goto.go index dccc286..3352f58 100644 --- a/internal/commands/goto.go +++ b/internal/commands/goto.go @@ -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" } diff --git a/internal/commands/if.go b/internal/commands/if.go index cfcb810..87ab0fb 100644 --- a/internal/commands/if.go +++ b/internal/commands/if.go @@ -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" } diff --git a/internal/commands/incr.go b/internal/commands/incr.go index 0752811..f112eb3 100644 --- a/internal/commands/incr.go +++ b/internal/commands/incr.go @@ -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" } diff --git a/internal/commands/label.go b/internal/commands/label.go index c6f9663..f420d20 100644 --- a/internal/commands/label.go +++ b/internal/commands/label.go @@ -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" } diff --git a/internal/commands/let.go b/internal/commands/let.go index eaee451..9dd57c3 100644 --- a/internal/commands/let.go +++ b/internal/commands/let.go @@ -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" } diff --git a/internal/commands/macro.go b/internal/commands/macro.go index 5c44895..19e68fd 100644 --- a/internal/commands/macro.go +++ b/internal/commands/macro.go @@ -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" } diff --git a/internal/commands/next.go b/internal/commands/next.go index 7442309..1ea97e9 100644 --- a/internal/commands/next.go +++ b/internal/commands/next.go @@ -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 diff --git a/internal/commands/or.go b/internal/commands/or.go index c94869b..a03a406 100644 --- a/internal/commands/or.go +++ b/internal/commands/or.go @@ -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" } diff --git a/internal/commands/origin.go b/internal/commands/origin.go index 1a069c9..29d39e7 100644 --- a/internal/commands/origin.go +++ b/internal/commands/origin.go @@ -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" } diff --git a/internal/commands/peek.go b/internal/commands/peek.go index 6ae3848..0024709 100644 --- a/internal/commands/peek.go +++ b/internal/commands/peek.go @@ -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) { diff --git a/internal/commands/peekw.go b/internal/commands/peekw.go index 35bd69e..6e43439 100644 --- a/internal/commands/peekw.go +++ b/internal/commands/peekw.go @@ -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) { diff --git a/internal/commands/point.go b/internal/commands/point.go index 3118ab9..a4e83a2 100644 --- a/internal/commands/point.go +++ b/internal/commands/point.go @@ -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" } diff --git a/internal/commands/poke.go b/internal/commands/poke.go index 49e913e..8a7f55d 100644 --- a/internal/commands/poke.go +++ b/internal/commands/poke.go @@ -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) { diff --git a/internal/commands/pokew.go b/internal/commands/pokew.go index 2767712..02dab72 100644 --- a/internal/commands/pokew.go +++ b/internal/commands/pokew.go @@ -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) { diff --git a/internal/commands/shiftl.go b/internal/commands/shiftl.go index 5ed6ca5..a4a3dce 100644 --- a/internal/commands/shiftl.go +++ b/internal/commands/shiftl.go @@ -209,9 +209,9 @@ func (c *ShiftLCommand) generateByteShift(ctx *compiler.CompilerContext) ([]stri asm = append(asm, "\tlda #0") asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) } - return asm, nil - } - + return asm, nil +} + // Shift 0 -> copy source if amount == 0 { return c.generateByteCopy(), nil @@ -311,10 +311,10 @@ func (c *ShiftLCommand) generateByteShiftVar(ctx *compiler.CompilerContext) ([]s // Store result back asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) - - return asm, nil - } + return asm, nil +} + // Different variables or literal: shift in accumulator (faster) // Generate labels loopLabel := ctx.GeneralStack.Push() @@ -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" } + diff --git a/internal/commands/shiftr.go b/internal/commands/shiftr.go index f62770c..23e3ee7 100644 --- a/internal/commands/shiftr.go +++ b/internal/commands/shiftr.go @@ -415,10 +415,10 @@ func (c *ShiftRCommand) generateWordToByteShift(ctx *compiler.CompilerContext) ( for i := uint16(0); i < remaining; i++ { asm = append(asm, fmt.Sprintf("\tlsr %s", c.destVarName)) } - - return asm, nil - } - + + return asm, nil +} + // Shift 1-7: need full WORD shift, then take low byte // We'll use the destination as a temporary for the low byte // and handle high byte in A register @@ -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" } diff --git a/internal/commands/subend.go b/internal/commands/subend.go index 61b011c..7b41011 100644 --- a/internal/commands/subend.go +++ b/internal/commands/subend.go @@ -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" } diff --git a/internal/commands/subtr.go b/internal/commands/subtr.go index 4ed8914..163983e 100644 --- a/internal/commands/subtr.go +++ b/internal/commands/subtr.go @@ -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" } diff --git a/internal/commands/switch.go b/internal/commands/switch.go index 56abba4..879a546 100644 --- a/internal/commands/switch.go +++ b/internal/commands/switch.go @@ -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" } diff --git a/internal/commands/wend.go b/internal/commands/wend.go index d4be077..8bc64a6 100644 --- a/internal/commands/wend.go +++ b/internal/commands/wend.go @@ -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" } diff --git a/internal/commands/while.go b/internal/commands/while.go index 5278867..50939a2 100644 --- a/internal/commands/while.go +++ b/internal/commands/while.go @@ -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" } diff --git a/internal/commands/word.go b/internal/commands/word.go index 78ac897..2ada2d7 100644 --- a/internal/commands/word.go +++ b/internal/commands/word.go @@ -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" } diff --git a/internal/commands/xor.go b/internal/commands/xor.go index 0431c33..f925a1d 100644 --- a/internal/commands/xor.go +++ b/internal/commands/xor.go @@ -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" } diff --git a/internal/compiler/command.go b/internal/compiler/command.go index d7ac6da..fbc4743 100644 --- a/internal/compiler/command.go +++ b/internal/compiler/command.go @@ -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 diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 815d51c..d92b324 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -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) { diff --git a/internal/compiler/compiler_test.go b/internal/compiler/compiler_test.go index af43c1b..f5e4276 100644 --- a/internal/compiler/compiler_test.go +++ b/internal/compiler/compiler_test.go @@ -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() diff --git a/internal/optimizer/config.go b/internal/optimizer/config.go new file mode 100644 index 0000000..49cf2e6 --- /dev/null +++ b/internal/optimizer/config.go @@ -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 +} diff --git a/internal/optimizer/debug.go b/internal/optimizer/debug.go new file mode 100644 index 0000000..629e1e8 --- /dev/null +++ b/internal/optimizer/debug.go @@ -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 +} diff --git a/internal/optimizer/optimizer.go b/internal/optimizer/optimizer.go new file mode 100644 index 0000000..ce96937 --- /dev/null +++ b/internal/optimizer/optimizer.go @@ -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) +} diff --git a/internal/optimizer/optimizer_test.go b/internal/optimizer/optimizer_test.go new file mode 100644 index 0000000..b377194 --- /dev/null +++ b/internal/optimizer/optimizer_test.go @@ -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) + } +} diff --git a/internal/optimizer/pass_imm.go b/internal/optimizer/pass_imm.go new file mode 100644 index 0000000..21af871 --- /dev/null +++ b/internal/optimizer/pass_imm.go @@ -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 +} diff --git a/internal/optimizer/pass_jmp.go b/internal/optimizer/pass_jmp.go new file mode 100644 index 0000000..5a7fe58 --- /dev/null +++ b/internal/optimizer/pass_jmp.go @@ -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 +} diff --git a/internal/optimizer/pass_load.go b/internal/optimizer/pass_load.go new file mode 100644 index 0000000..b4164d5 --- /dev/null +++ b/internal/optimizer/pass_load.go @@ -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] == '#' +} diff --git a/internal/optimizer/pass_self.go b/internal/optimizer/pass_self.go new file mode 100644 index 0000000..0e09552 --- /dev/null +++ b/internal/optimizer/pass_self.go @@ -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 +} diff --git a/internal/optimizer/regstate.go b/internal/optimizer/regstate.go new file mode 100644 index 0000000..6687c7e --- /dev/null +++ b/internal/optimizer/regstate.go @@ -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 ®State{ + 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 + } +} diff --git a/internal/optimizer/scanner.go b/internal/optimizer/scanner.go new file mode 100644 index 0000000..6af88bd --- /dev/null +++ b/internal/optimizer/scanner.go @@ -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 +} +