Compare commits
No commits in common. "c76c514d35de1f72016afe459ddfcfe247a938cb" and "59b53fb5f0c68a9be1aeb72566edc33a25703cb5" have entirely different histories.
c76c514d35
...
59b53fb5f0
55 changed files with 30 additions and 1976 deletions
109
README.md
109
README.md
|
|
@ -201,115 +201,6 @@ c65gm automatically:
|
||||||
|
|
||||||
**Error Handling:** If ACME is not found, c65gm shows installation instructions and suggests using `compile` mode instead.
|
**Error Handling:** If ACME is not found, c65gm shows installation instructions and suggests using `compile` mode instead.
|
||||||
|
|
||||||
## Peephole Optimizer (Experimental)
|
|
||||||
|
|
||||||
> **⚠️ Experimental.** This feature is new and actively being developed. All optimizations are **disabled by default** and must be explicitly enabled. While thoroughly tested on real C64 projects, edge cases may exist — always verify your program's behavior when optimization is active.
|
|
||||||
|
|
||||||
c65gm includes a peephole optimizer that removes redundant 6502 instructions from the generated assembly. It operates as a post-pass on the compiler output, scanning instruction windows and applying pattern-matching rules where safe.
|
|
||||||
|
|
||||||
The optimizer only modifies **generated code** — hand-written `ASM`/`ENDASM` blocks, `SCRIPT` output, and inline assembly are left untouched. It can also be configured to skip memory regions used for I/O (e.g., `$D000-$DFFF` on the C64).
|
|
||||||
|
|
||||||
### Optimization Passes
|
|
||||||
|
|
||||||
Five passes run in sequence. All are enabled by default when `--opt` is used.
|
|
||||||
|
|
||||||
| Pass | Pragma | Name | What it removes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | `_P_OPT_STLD` | Store-then-load | `sta X; ...; lda X` → `sta X` when A wasn't modified between |
|
|
||||||
| 2 | `_P_OPT_LOAD` | Redundant load | `lda X; ...; lda X` → `lda X` when A already holds X's value |
|
|
||||||
| 3 | `_P_OPT_IMM` | Redundant immediate | `lda #N; ...; lda #N` → `lda #N` when A already holds N |
|
|
||||||
| 4 | `_P_OPT_JMP` | Jump-to-next | `jmp L; L:` → `L:` when no code between `jmp` and its target |
|
|
||||||
| 5 | `_P_OPT_SELF` | Self-assignment | `lda X; sta X` → (removed) when same variable stored back to itself |
|
|
||||||
|
|
||||||
### CLI Flags
|
|
||||||
|
|
||||||
Available on the `build` and `compile` subcommands:
|
|
||||||
|
|
||||||
```
|
|
||||||
c65gm build -i program.c65 --opt [--opt-debug] [--opt-exclude ...] [--opt-exclude-c64-io]
|
|
||||||
c65gm compile -i program.c65 --opt [--opt-debug] [--opt-exclude ...] [--opt-exclude-c64-io]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Flag | Effect |
|
|
||||||
|---|---|
|
|
||||||
| `--opt` | Enable all 5 optimization passes (equivalent to `#PRAGMA _P_OPT_ALL 1`) |
|
|
||||||
| `--opt-debug` | Annotate the assembly output with `[removed]` markers and a summary line showing how many instructions were saved |
|
|
||||||
| `--opt-exclude` | Mark an address range as I/O to exclude from store-load optimization. Repeatable. Format: `START:END` with hex (`0x` prefix) or decimal values. |
|
|
||||||
| `--opt-exclude-c64-io` | Shorthand for `--opt-exclude 0xD000:0xDFFF`. Convenience for C64 targets. |
|
|
||||||
|
|
||||||
### Pragma Equivalents
|
|
||||||
|
|
||||||
All CLI flags have equivalent `#PRAGMA` directives in the source code:
|
|
||||||
|
|
||||||
```c65
|
|
||||||
#PRAGMA _P_OPT_ALL 1 ; enable all passes
|
|
||||||
#PRAGMA _P_OPT_LOAD 1 ; enable redundant load pass only
|
|
||||||
#PRAGMA _P_OPT_IMM 1 ; enable redundant immediate pass only
|
|
||||||
#PRAGMA _P_OPT_JMP 1 ; enable jump-to-next pass only
|
|
||||||
#PRAGMA _P_OPT_SELF 1 ; enable self-assignment pass only
|
|
||||||
#PRAGMA _P_OPT_STLD 1 ; enable store-then-load pass only
|
|
||||||
#PRAGMA _P_OPT_DEBUG 1 ; annotate output with [removed] markers
|
|
||||||
#PRAGMA _P_OPT_IO $D000 $DFFF ; mark C64 I/O range as excluded
|
|
||||||
```
|
|
||||||
|
|
||||||
Pragmas and CLI flags are **additive** — both can be used together.
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable all passes
|
|
||||||
c65gm compile -i game.c65 --opt -o game.asm
|
|
||||||
|
|
||||||
# Enable passes with debug annotations (see what was removed)
|
|
||||||
c65gm compile -i game.c65 --opt --opt-debug -o game.asm
|
|
||||||
|
|
||||||
# Enable passes, protect C64 I/O region (recommended for C64 targets)
|
|
||||||
c65gm build -i game.c65 --opt --opt-exclude-c64-io
|
|
||||||
|
|
||||||
# Protect custom I/O ranges (e.g., multiple VIC-II register blocks)
|
|
||||||
c65gm compile -i game.c65 --opt --opt-exclude 0xD000:0xDFFF --opt-exclude 0xDC00:0xDC0F
|
|
||||||
|
|
||||||
# Combined: all passes + debug + I/O protection
|
|
||||||
c65gm build -i game.c65 --opt --opt-debug --opt-exclude-c64-io
|
|
||||||
|
|
||||||
# Command-line equivalent to #PRAGMA _P_OPT_ALL + #PRAGMA _P_OPT_IO $D000 $DFFF
|
|
||||||
c65gm build -i game.c65 --opt --opt-exclude 0xD000:0xDFFF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verifying Optimizations
|
|
||||||
|
|
||||||
With `--opt-debug`, the assembly output contains:
|
|
||||||
|
|
||||||
```
|
|
||||||
; --- peephole: 4311 asm lines → 4072 (239 saved) ---
|
|
||||||
...
|
|
||||||
; POKE target_addr , value
|
|
||||||
; [removed] ldy #0
|
|
||||||
lda mem_copy_value
|
|
||||||
sta (mem_copy_target_addr),y
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
The header shows the instruction count before and after (`X asm lines → Y`), plus the total saved. Each `[removed]` annotation marks an instruction that was eliminated. The annotation is a comment and does not affect assembly.
|
|
||||||
|
|
||||||
For a full difference view, compile with and without `--opt` and diff the two `.asm` files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
c65gm compile -i game.c65 -o baseline.asm
|
|
||||||
c65gm compile -i game.c65 --opt --opt-debug -o optimized.asm
|
|
||||||
diff -u baseline.asm optimized.asm | less
|
|
||||||
```
|
|
||||||
|
|
||||||
### I/O Safety
|
|
||||||
|
|
||||||
The optimizer never removes **reads or stores** to addresses marked in the I/O exclusion map. Reading an I/O register (like `$D012` raster line or `$DC01` keyboard matrix) may return different values each time, so redundant load elimination is disabled for those addresses. These can be specified via:
|
|
||||||
|
|
||||||
1. **CLI flag**: `--opt-exclude 0xD000:0xDFFF` (repeatable for multiple regions)
|
|
||||||
2. **CLI shorthand**: `--opt-exclude-c64-io` for the full C64 I/O page
|
|
||||||
3. **Pragma**: `#PRAGMA _P_OPT_IO $D000 $DFFF`
|
|
||||||
|
|
||||||
Variable names (like `vic2`, `BORDER_COLOR`) are NOT checked against the I/O map — only literal hex addresses are. For `@`-mapped variables that point to I/O registers, use the `_P_OPT_IO` pragma with their address range.
|
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
- **`C65LIBPATH`**: Search path for `#INCLUDE <file>` directives
|
- **`C65LIBPATH`**: Search path for `#INCLUDE <file>` directives
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,3 @@ func (c *AddCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AddCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *AddCommand) GetName() string { return "ADD" }
|
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,3 @@ func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AndCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *AndCommand) GetName() string { return "AND" }
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,3 @@ func (c *BreakCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
fmt.Sprintf("\tjmp %s", c.skipLabel),
|
fmt.Sprintf("\tjmp %s", c.skipLabel),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BreakCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *BreakCommand) GetName() string { return "BREAK" }
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,3 @@ func (c *ByteCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
// Variables are rendered by assembleOutput, not by individual commands
|
// Variables are rendered by assembleOutput, not by individual commands
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ByteCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *ByteCommand) GetName() string { return "BYTE" }
|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,3 @@ func (c *CallCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
|
||||||
func (c *CallCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
func (c *CallCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
return c.asmOutput, nil
|
return c.asmOutput, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CallCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *CallCommand) GetName() string { return "CALL" }
|
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,3 @@ func (c *CaseCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CaseCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *CaseCommand) GetName() string { return "CASE" }
|
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,3 @@ func (c *DecrCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DecrCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *DecrCommand) GetName() string { return "DEC" }
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,3 @@ func (c *DefaultCommand) Generate(ctx *compiler.CompilerContext) ([]string, erro
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *DefaultCommand) GetName() string { return "DEFAULT" }
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,3 @@ func (c *ElseCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
c.skipLabel,
|
c.skipLabel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ElseCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *ElseCommand) GetName() string { return "ELSE" }
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,3 @@ func (c *EndIfCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContex
|
||||||
func (c *EndIfCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
func (c *EndIfCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
return []string{c.endLabel}, nil
|
return []string{c.endLabel}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EndIfCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *EndIfCommand) GetName() string { return "ENDIF" }
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,3 @@ func (c *EndSwitchCommand) Generate(ctx *compiler.CompilerContext) ([]string, er
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EndSwitchCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *EndSwitchCommand) GetName() string { return "ENDSWITCH" }
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,3 @@ func (c *FendCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
}
|
}
|
||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FendCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
|
|
||||||
func (c *FendCommand) GetName() string { return "FEND" }
|
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,3 @@ func (c *ForCommand) generateAssignment() []string {
|
||||||
|
|
||||||
return asm
|
return asm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ForCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *ForCommand) GetName() string { return "FOR" }
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,3 @@ func (c *FuncCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
marker := fmt.Sprintf("; @@FUNC_BEGIN %s", c.funcName)
|
marker := fmt.Sprintf("; @@FUNC_BEGIN %s", c.funcName)
|
||||||
return []string{marker, c.funcName}, nil
|
return []string{marker, c.funcName}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FuncCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
|
|
||||||
func (c *FuncCommand) GetName() string { return "FUNC" }
|
|
||||||
|
|
|
||||||
|
|
@ -222,9 +222,6 @@ func (c *GosubCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GosubCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *GosubCommand) GetName() string { return "GOSUB" }
|
|
||||||
|
|
||||||
func (c *GosubCommand) generatePrePassing() []string {
|
func (c *GosubCommand) generatePrePassing() []string {
|
||||||
var asm []string
|
var asm []string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,3 @@ func (c *GotoCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
// Jump to computed address
|
// Jump to computed address
|
||||||
return []string{fmt.Sprintf("\tjmp $%04x", c.address)}, nil
|
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" }
|
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,3 @@ func (c *IfCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||||
|
|
||||||
return cmpAsm, nil
|
return cmpAsm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *IfCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *IfCommand) GetName() string { return "IF" }
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,3 @@ func (c *IncrCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *IncrCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *IncrCommand) GetName() string { return "INC" }
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,3 @@ func (c *LabelCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext)
|
||||||
func (c *LabelCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
func (c *LabelCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
return []string{c.labelName}, nil
|
return []string{c.labelName}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LabelCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
|
|
||||||
func (c *LabelCommand) GetName() string { return "LABEL" }
|
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,3 @@ func (c *LetCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LetCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *LetCommand) GetName() string { return "LET" }
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,3 @@ func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
result = append(result, fmt.Sprintf("; end @%s", c.macroName))
|
result = append(result, fmt.Sprintf("; end @%s", c.macroName))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MacroCommand) GetClass() compiler.CmdClass { return compiler.CmdBoundary }
|
|
||||||
func (c *MacroCommand) GetName() string { return "MACRO" }
|
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,6 @@ func (c *NextCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
return asm, nil
|
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.
|
// generateEndCheck generates code to exit if var == end.
|
||||||
func (c *NextCommand) generateEndCheck(ctx *compiler.CompilerContext) []string {
|
func (c *NextCommand) generateEndCheck(ctx *compiler.CompilerContext) []string {
|
||||||
var asm []string
|
var asm []string
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,3 @@ func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OrCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *OrCommand) GetName() string { return "OR" }
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,3 @@ func (c *OriginCommand) Interpret(line preproc.Line, ctx *compiler.CompilerConte
|
||||||
func (c *OriginCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
func (c *OriginCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
return []string{fmt.Sprintf("*= $%04x", c.address)}, nil
|
return []string{fmt.Sprintf("*= $%04x", c.address)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OriginCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *OriginCommand) GetName() string { return "ORIGIN" }
|
|
||||||
|
|
|
||||||
|
|
@ -280,17 +280,6 @@ func (c *PeekCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
return asm, nil
|
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]"
|
// parseOffset extracts offset from syntax like "var[offset]"
|
||||||
// Returns (base, offset) where offset is empty if no brackets found
|
// Returns (base, offset) where offset is empty if no brackets found
|
||||||
func parseOffset(param string) (base string, offset string) {
|
func parseOffset(param string) (base string, offset string) {
|
||||||
|
|
|
||||||
|
|
@ -281,17 +281,6 @@ func (c *PeekWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
return asm, nil
|
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]"
|
// parseOffsetW extracts offset from syntax like "var[offset]"
|
||||||
// Returns (base, offset) where offset is empty if no brackets found
|
// Returns (base, offset) where offset is empty if no brackets found
|
||||||
func parseOffsetW(param string) (base string, offset string) {
|
func parseOffsetW(param string) (base string, offset string) {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,3 @@ func (c *PointerCommand) Generate(ctx *compiler.CompilerContext) ([]string, erro
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PointerCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *PointerCommand) GetName() string { return "POINT" }
|
|
||||||
|
|
|
||||||
|
|
@ -258,17 +258,6 @@ func (c *PokeCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
return asm, nil
|
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]"
|
// parsePOKEOffset extracts offset from syntax like "var[offset]"
|
||||||
// Returns (base, offset) where offset is empty if no brackets found
|
// Returns (base, offset) where offset is empty if no brackets found
|
||||||
func parsePOKEOffset(param string) (base string, offset string) {
|
func parsePOKEOffset(param string) (base string, offset string) {
|
||||||
|
|
|
||||||
|
|
@ -294,17 +294,6 @@ func (c *PokeWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
return asm, nil
|
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]"
|
// parsePOKEWOffset extracts offset from syntax like "var[offset]"
|
||||||
// Returns (base, offset) where offset is empty if no brackets found
|
// Returns (base, offset) where offset is empty if no brackets found
|
||||||
func parsePOKEWOffset(param string) (base string, offset string) {
|
func parsePOKEWOffset(param string) (base string, offset string) {
|
||||||
|
|
|
||||||
|
|
@ -209,8 +209,8 @@ func (c *ShiftLCommand) generateByteShift(ctx *compiler.CompilerContext) ([]stri
|
||||||
asm = append(asm, "\tlda #0")
|
asm = append(asm, "\tlda #0")
|
||||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||||
}
|
}
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shift 0 -> copy source
|
// Shift 0 -> copy source
|
||||||
if amount == 0 {
|
if amount == 0 {
|
||||||
|
|
@ -312,8 +312,8 @@ func (c *ShiftLCommand) generateByteShiftVar(ctx *compiler.CompilerContext) ([]s
|
||||||
// Store result back
|
// Store result back
|
||||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Different variables or literal: shift in accumulator (faster)
|
// Different variables or literal: shift in accumulator (faster)
|
||||||
// Generate labels
|
// Generate labels
|
||||||
|
|
@ -510,6 +510,3 @@ func (c *ShiftLCommand) generateWordShiftVar(ctx *compiler.CompilerContext) ([]s
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ShiftLCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *ShiftLCommand) GetName() string { return "SHL" }
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -416,8 +416,8 @@ func (c *ShiftRCommand) generateWordToByteShift(ctx *compiler.CompilerContext) (
|
||||||
asm = append(asm, fmt.Sprintf("\tlsr %s", c.destVarName))
|
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
|
// Shift 1-7: need full WORD shift, then take low byte
|
||||||
// We'll use the destination as a temporary for the low byte
|
// We'll use the destination as a temporary for the low byte
|
||||||
|
|
@ -653,5 +653,3 @@ func (c *ShiftRCommand) generateWordShiftVar(ctx *compiler.CompilerContext) ([]s
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ShiftRCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *ShiftRCommand) GetName() string { return "SHR" }
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,3 @@ func (c *SubEndCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext
|
||||||
func (c *SubEndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
func (c *SubEndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
return []string{"\trts"}, nil
|
return []string{"\trts"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SubEndCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *SubEndCommand) GetName() string { return "SUBEND" }
|
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,3 @@ func (c *SubtractCommand) Generate(_ *compiler.CompilerContext) ([]string, error
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SubtractCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *SubtractCommand) GetName() string { return "SUB" }
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,3 @@ func (c *SwitchCommand) Generate(ctx *compiler.CompilerContext) ([]string, error
|
||||||
// The variable will be loaded and compared by each CASE
|
// The variable will be loaded and compared by each CASE
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SwitchCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *SwitchCommand) GetName() string { return "SWITCH" }
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,3 @@ func (c *WendCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
c.skipLabel,
|
c.skipLabel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WendCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *WendCommand) GetName() string { return "WEND" }
|
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,3 @@ func (c *WhileCommand) Generate(ctx *compiler.CompilerContext) ([]string, error)
|
||||||
asm = append(asm, cmpAsm...)
|
asm = append(asm, cmpAsm...)
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhileCommand) GetClass() compiler.CmdClass { return compiler.CmdFlowCtrl }
|
|
||||||
func (c *WhileCommand) GetName() string { return "WHILE" }
|
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,3 @@ func (c *WordCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
// Variables are rendered by assembleOutput, not by individual commands
|
// Variables are rendered by assembleOutput, not by individual commands
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WordCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *WordCommand) GetName() string { return "WORD" }
|
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,3 @@ func (c *XorCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XorCommand) GetClass() compiler.CmdClass { return compiler.CmdLinear }
|
|
||||||
func (c *XorCommand) GetName() string { return "XOR" }
|
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,6 @@ import (
|
||||||
"c65gm/internal/preproc"
|
"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
|
// Command represents a script command that can interpret source lines and generate assembly
|
||||||
type Command interface {
|
type Command interface {
|
||||||
// WillHandle checks if this command can handle the given line
|
// WillHandle checks if this command can handle the given line
|
||||||
|
|
@ -29,12 +20,6 @@ type Command interface {
|
||||||
// Generate produces assembly output based on previously interpreted line
|
// Generate produces assembly output based on previously interpreted line
|
||||||
// Returns assembly lines and error if generation fails
|
// Returns assembly lines and error if generation fails
|
||||||
Generate(ctx *CompilerContext) ([]string, error)
|
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
|
// CommandRegistry manages registered commands and dispatches lines to appropriate handlers
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,14 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"c65gm/internal/optimizer"
|
|
||||||
"c65gm/internal/preproc"
|
"c65gm/internal/preproc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compiler orchestrates the compilation process
|
// Compiler orchestrates the compilation process
|
||||||
type Compiler struct {
|
type Compiler struct {
|
||||||
ctx *CompilerContext
|
ctx *CompilerContext
|
||||||
registry *CommandRegistry
|
registry *CommandRegistry
|
||||||
deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma
|
deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma
|
||||||
CmdlineOpt bool // --opt enables all passes
|
|
||||||
CmdlineDebug bool // --opt-debug enables debug output
|
|
||||||
CmdlineIORegions []optimizer.IORegion // --opt-exclude ranges
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCompiler creates a new compiler with initialized context and registry
|
// NewCompiler creates a new compiler with initialized context and registry
|
||||||
|
|
@ -237,9 +233,6 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
|
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
|
||||||
if len(asmLines) > 0 && c.isMarkersEnabled() {
|
|
||||||
codeOutput = append(codeOutput, fmt.Sprintf("; @@OPT:%s:%s", classString(cmd.GetClass()), cmd.GetName()))
|
|
||||||
}
|
|
||||||
codeOutput = append(codeOutput, asmLines...)
|
codeOutput = append(codeOutput, asmLines...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,11 +251,6 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
return nil, fmt.Errorf("Unclosed SCRIPT MACRO block.")
|
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
|
// Analyze for overlapping absolute addresses in function call chains
|
||||||
c.checkAbsoluteOverlaps()
|
c.checkAbsoluteOverlaps()
|
||||||
|
|
||||||
|
|
@ -284,117 +272,10 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
// Remove unused functions with _P_REMOVE_UNUSED pragma
|
// Remove unused functions with _P_REMOVE_UNUSED pragma
|
||||||
codeOutput, removedFuncs := c.removeUnusedFunctions(codeOutput)
|
codeOutput, removedFuncs := c.removeUnusedFunctions(codeOutput)
|
||||||
|
|
||||||
// Update peephole header to match actual [removed] count after function removal
|
|
||||||
codeOutput = updatePeepholeHeader(codeOutput)
|
|
||||||
|
|
||||||
// Assemble final output with headers and footers
|
// Assemble final output with headers and footers
|
||||||
return c.assembleOutput(codeOutput, removedFuncs), nil
|
return c.assembleOutput(codeOutput, removedFuncs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isOptimizing returns true if any peephole optimization pragma is active
|
|
||||||
func (c *Compiler) isOptimizing() bool {
|
|
||||||
if c.CmdlineOpt {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// isMarkersEnabled returns true if _P_OPT_MARKERS is active
|
|
||||||
func (c *Compiler) isMarkersEnabled() bool {
|
|
||||||
if c.ctx == nil || c.ctx.Pragma == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
idx := c.ctx.Pragma.GetCurrentPragmaSetIndex()
|
|
||||||
ps := c.ctx.Pragma.GetPragmaSetByIndex(idx)
|
|
||||||
return ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Command-line overrides
|
|
||||||
if c.CmdlineOpt {
|
|
||||||
cfg.EnableLoad = true
|
|
||||||
cfg.EnableImm = true
|
|
||||||
cfg.EnableJmp = true
|
|
||||||
cfg.EnableSelf = true
|
|
||||||
cfg.EnableStoreLoad = true
|
|
||||||
}
|
|
||||||
if c.CmdlineDebug {
|
|
||||||
cfg.Debug = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.Any() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply CLI I/O regions before pragma-based ones
|
|
||||||
if len(c.CmdlineIORegions) > 0 {
|
|
||||||
cfg.AddIORegions(c.CmdlineIORegions)
|
|
||||||
}
|
|
||||||
cfg.BuildIOMap(c.ctx.Pragma)
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// updatePeepholeHeader recalculates the peephole header saved count
|
|
||||||
// from the actual [removed] annotations present after removeUnusedFunctions.
|
|
||||||
func updatePeepholeHeader(codeOutput []string) []string {
|
|
||||||
headerIdx := -1
|
|
||||||
var header string
|
|
||||||
removedCount := 0
|
|
||||||
|
|
||||||
for i, line := range codeOutput {
|
|
||||||
if strings.HasPrefix(line, "; --- peephole:") {
|
|
||||||
headerIdx = i
|
|
||||||
header = line
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "; [removed]") {
|
|
||||||
removedCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if headerIdx < 0 {
|
|
||||||
return codeOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract orig count from the existing header
|
|
||||||
var origCount, oldOptCount int
|
|
||||||
if _, err := fmt.Sscanf(header, "; --- peephole: %d asm lines → %d",
|
|
||||||
&origCount, &oldOptCount); err != nil {
|
|
||||||
return codeOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
newHeader := fmt.Sprintf("; --- peephole: %d asm lines → %d (%d saved) ---",
|
|
||||||
origCount, origCount-removedCount, removedCount)
|
|
||||||
codeOutput[headerIdx] = newHeader
|
|
||||||
return codeOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// printErrorWithContext prints an error with source code context to stderr
|
||||||
func (c *Compiler) printErrorWithContext(lines []preproc.Line, lineIndex int, err error) {
|
func (c *Compiler) printErrorWithContext(lines []preproc.Line, lineIndex int, err error) {
|
||||||
if lineIndex < 0 || lineIndex >= len(lines) {
|
if lineIndex < 0 || lineIndex >= len(lines) {
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,6 @@ func (c *TestBreakCommand) Generate(ctx *CompilerContext) ([]string, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TestBreakCommand) GetClass() CmdClass { return CmdLinear }
|
|
||||||
func (c *TestBreakCommand) GetName() string { return "BREAK" }
|
|
||||||
|
|
||||||
func TestCompilerArchitecture(t *testing.T) {
|
func TestCompilerArchitecture(t *testing.T) {
|
||||||
// Create pragma
|
// Create pragma
|
||||||
pragma := preproc.NewPragma()
|
pragma := preproc.NewPragma()
|
||||||
|
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
package optimizer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"c65gm/internal/preproc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
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"),
|
|
||||||
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 || 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IORegion defines an address range to exclude from optimization.
|
|
||||||
type IORegion struct {
|
|
||||||
Start, End uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddIORegions marks all addresses in the given regions as I/O in the IOMap.
|
|
||||||
func (c *Config) AddIORegions(regions []IORegion) {
|
|
||||||
for _, r := range regions {
|
|
||||||
for a := r.Start; a <= r.End; a++ {
|
|
||||||
c.IOMap[a] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseExcludeRange parses a string like "0xD000:0xDFFF" or "53248:57343".
|
|
||||||
// Returns an IORegion or an error.
|
|
||||||
func ParseExcludeRange(s string) (IORegion, error) {
|
|
||||||
parts := strings.SplitN(s, ":", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return IORegion{}, fmt.Errorf("expected START:END, got %q", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
start, err := parseExcludeAddr(parts[0])
|
|
||||||
if err != nil {
|
|
||||||
return IORegion{}, fmt.Errorf("invalid start %q: %w", parts[0], err)
|
|
||||||
}
|
|
||||||
end, err := parseExcludeAddr(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return IORegion{}, fmt.Errorf("invalid end %q: %w", parts[1], err)
|
|
||||||
}
|
|
||||||
if start > end {
|
|
||||||
return IORegion{}, fmt.Errorf("start %s > end %s", parts[0], parts[1])
|
|
||||||
}
|
|
||||||
return IORegion{Start: start, End: end}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseExcludeAddr(s string) (uint16, error) {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
// Hex: 0x prefix (case-insensitive)
|
|
||||||
if len(s) > 2 && (s[:2] == "0x" || s[:2] == "0X") {
|
|
||||||
v, err := strconv.ParseUint(s[2:], 16, 16)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint16(v), nil
|
|
||||||
}
|
|
||||||
// Decimal
|
|
||||||
v, err := strconv.ParseUint(s, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint16(v), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
package optimizer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"c65gm/internal/preproc"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewConfigWithAll(t *testing.T) {
|
|
||||||
ps := preproc.NewPragmaSet(map[string]string{
|
|
||||||
"_P_OPT_ALL": "1",
|
|
||||||
"_P_OPT_DEBUG": "1",
|
|
||||||
})
|
|
||||||
cfg := NewConfig(ps)
|
|
||||||
if !cfg.Any() {
|
|
||||||
t.Error("expected Any()=true")
|
|
||||||
}
|
|
||||||
if !cfg.EnableLoad {
|
|
||||||
t.Error("expected EnableLoad=true from _P_OPT_ALL")
|
|
||||||
}
|
|
||||||
if !cfg.EnableImm {
|
|
||||||
t.Error("expected EnableImm=true from _P_OPT_ALL")
|
|
||||||
}
|
|
||||||
if !cfg.Debug {
|
|
||||||
t.Error("expected Debug=true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseExcludeRange(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
wantOk bool
|
|
||||||
wantS uint16
|
|
||||||
wantE uint16
|
|
||||||
}{
|
|
||||||
{"hex", "0xD000:0xDFFF", true, 0xD000, 0xDFFF},
|
|
||||||
{"hex lowercase", "0xd000:0xdfff", true, 0xD000, 0xDFFF},
|
|
||||||
{"hex mixed case", "0XD000:0XdFfF", true, 0xD000, 0xDFFF},
|
|
||||||
{"decimal", "53248:57343", true, 53248, 57343},
|
|
||||||
{"single address", "0xD020:0xD020", true, 0xD020, 0xD020},
|
|
||||||
{"invalid range", "0xFFFF:0x0000", false, 0, 0},
|
|
||||||
{"bad format", "garbage", false, 0, 0},
|
|
||||||
{"bad hex", "0xGGGG:0x0000", false, 0, 0},
|
|
||||||
{"too many colons", "0xD000:0xDFFF:extra", false, 0, 0},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r, err := ParseExcludeRange(tt.input)
|
|
||||||
if tt.wantOk {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if r.Start != tt.wantS || r.End != tt.wantE {
|
|
||||||
t.Errorf("got %04X:%04X, want %04X:%04X", r.Start, r.End, tt.wantS, tt.wantE)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error, got %04X:%04X", r.Start, r.End)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddIORegions(t *testing.T) {
|
|
||||||
cfg := &Config{}
|
|
||||||
cfg.AddIORegions([]IORegion{
|
|
||||||
{Start: 0xD000, End: 0xDFFF},
|
|
||||||
})
|
|
||||||
|
|
||||||
if !cfg.IOMap[0xD000] {
|
|
||||||
t.Error("expected 0xD000 to be marked I/O")
|
|
||||||
}
|
|
||||||
if !cfg.IOMap[0xDFFF] {
|
|
||||||
t.Error("expected 0xDFFF to be marked I/O")
|
|
||||||
}
|
|
||||||
if cfg.IOMap[0xCFFF] {
|
|
||||||
t.Error("expected 0xCFFF to NOT be marked I/O")
|
|
||||||
}
|
|
||||||
if cfg.IOMap[0xE000] {
|
|
||||||
t.Error("expected 0xE000 to NOT be marked I/O")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddIORegionsMultiple(t *testing.T) {
|
|
||||||
cfg := &Config{}
|
|
||||||
cfg.AddIORegions([]IORegion{
|
|
||||||
{Start: 0xD000, End: 0xDFFF},
|
|
||||||
{Start: 0xDC00, End: 0xDC0F}, // overlaps — still correct
|
|
||||||
})
|
|
||||||
|
|
||||||
if !cfg.IOMap[0xD000] {
|
|
||||||
t.Error("expected 0xD000 I/O")
|
|
||||||
}
|
|
||||||
if !cfg.IOMap[0xDC00] {
|
|
||||||
t.Error("expected 0xDC00 I/O")
|
|
||||||
}
|
|
||||||
if !cfg.IOMap[0xDC0F] {
|
|
||||||
t.Error("expected 0xDC0F I/O")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildIOMap(t *testing.T) {
|
|
||||||
p := preproc.NewPragma()
|
|
||||||
p.AddPragma("_P_OPT_IO", "$D000 $DFFF")
|
|
||||||
|
|
||||||
// Last pragma set should have _P_OPT_IO
|
|
||||||
ps := p.GetPragmaSetByIndex(p.GetCurrentPragmaSetIndex())
|
|
||||||
cfg := NewConfig(ps)
|
|
||||||
cfg.BuildIOMap(p)
|
|
||||||
|
|
||||||
if !cfg.IOMap[0xD000] {
|
|
||||||
t.Error("expected $D000 I/O from pragma")
|
|
||||||
}
|
|
||||||
if !cfg.IOMap[0xDFFF] {
|
|
||||||
t.Error("expected $DFFF I/O from pragma")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildIOMapMultiple(t *testing.T) {
|
|
||||||
// Simulate two _P_OPT_IO pragmas
|
|
||||||
p := preproc.NewPragma()
|
|
||||||
p.AddPragma("_P_OPT_IO", "$D000 $DFFF")
|
|
||||||
p.AddPragma("_P_OPT_IO", "$DC00 $DC0F")
|
|
||||||
p.AddPragma("_P_SOMETHING_ELSE", "1")
|
|
||||||
|
|
||||||
// BuildIOMap scans ALL sets, finding _P_OPT_IO at index 1 and 2
|
|
||||||
ps := p.GetPragmaSetByIndex(p.GetCurrentPragmaSetIndex())
|
|
||||||
cfg := NewConfig(ps)
|
|
||||||
cfg.BuildIOMap(p)
|
|
||||||
|
|
||||||
if !cfg.IOMap[0xD000] {
|
|
||||||
t.Error("expected $D000 I/O")
|
|
||||||
}
|
|
||||||
if !cfg.IOMap[0xDC00] {
|
|
||||||
t.Error("expected $DC00 I/O")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildIOMapEmpty(t *testing.T) {
|
|
||||||
p := preproc.NewPragma()
|
|
||||||
ps := p.GetPragmaSetByIndex(p.GetCurrentPragmaSetIndex())
|
|
||||||
cfg := NewConfig(ps)
|
|
||||||
cfg.BuildIOMap(p)
|
|
||||||
|
|
||||||
for _, addr := range []uint16{0, 0xD000, 0xFFFF} {
|
|
||||||
if cfg.IOMap[addr] {
|
|
||||||
t.Errorf("address %04X should not be I/O", addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewConfigPragmasAccumulate(t *testing.T) {
|
|
||||||
// Pragma sets COPY previous values — so last set has ALL pragmas
|
|
||||||
p := preproc.NewPragma()
|
|
||||||
p.AddPragma("_P_OPT_ALL", "1")
|
|
||||||
p.AddPragma("_P_OPT_DEBUG", "1")
|
|
||||||
p.AddPragma("_P_SOMETHING_ELSE", "1")
|
|
||||||
|
|
||||||
// Last set should have all three
|
|
||||||
ps := p.GetPragmaSetByIndex(p.GetCurrentPragmaSetIndex())
|
|
||||||
if v := ps.GetPragma("_P_OPT_ALL"); v != "1" {
|
|
||||||
t.Errorf("expected _P_OPT_ALL=1, got %q", v)
|
|
||||||
}
|
|
||||||
if v := ps.GetPragma("_P_OPT_DEBUG"); v != "1" {
|
|
||||||
t.Errorf("expected _P_OPT_DEBUG=1, got %q", v)
|
|
||||||
}
|
|
||||||
if v := ps.GetPragma("_P_SOMETHING_ELSE"); v != "1" {
|
|
||||||
t.Errorf("expected _P_SOMETHING_ELSE=1, got %q", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := NewConfig(ps)
|
|
||||||
if !cfg.Any() {
|
|
||||||
t.Error("expected Any()=true")
|
|
||||||
}
|
|
||||||
if !cfg.Debug {
|
|
||||||
t.Error("expected Debug=true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
package optimizer
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// debugDiff produces annotated output showing original vs optimized code.
|
|
||||||
// Lines that were removed are shown with "[removed]" annotation.
|
|
||||||
func debugDiff(original, optimized []asmLine) []asmLine {
|
|
||||||
// Count [removed] annotations from the walk, then derive optCount from
|
|
||||||
// origCount - removedCount. This avoids mismatches when later pipeline
|
|
||||||
// steps (e.g. removeUnusedFunctions) remove code that the optimizer saw.
|
|
||||||
origCount := countCodeLines(original)
|
|
||||||
|
|
||||||
var body []asmLine
|
|
||||||
oi := 0
|
|
||||||
opi := 0
|
|
||||||
removedCount := 0
|
|
||||||
|
|
||||||
for oi < len(original) {
|
|
||||||
origLine := original[oi]
|
|
||||||
|
|
||||||
if opi < len(optimized) && linesMatch(origLine, optimized[opi]) {
|
|
||||||
body = append(body, optimized[opi])
|
|
||||||
oi++
|
|
||||||
opi++
|
|
||||||
} else if !origLine.isCode {
|
|
||||||
body = append(body, origLine)
|
|
||||||
oi++
|
|
||||||
} else {
|
|
||||||
body = append(body, asmLine{
|
|
||||||
text: fmt.Sprintf("; [removed] %s", origLine.text),
|
|
||||||
isComment: true,
|
|
||||||
})
|
|
||||||
removedCount++
|
|
||||||
oi++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; opi < len(optimized); opi++ {
|
|
||||||
body = append(body, optimized[opi])
|
|
||||||
}
|
|
||||||
|
|
||||||
optCount := origCount - removedCount
|
|
||||||
header := fmt.Sprintf("; --- peephole: %d asm lines → %d (%d saved) ---",
|
|
||||||
origCount, optCount, removedCount)
|
|
||||||
|
|
||||||
result := []asmLine{{text: header, isComment: true}}
|
|
||||||
result = append(result, body...)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// linesMatch returns true if two asmLines represent the same instruction.
|
|
||||||
func linesMatch(a, b asmLine) bool {
|
|
||||||
return a.isCode == b.isCode && a.isLabel == b.isLabel && a.isComment == b.isComment && a.text == b.text
|
|
||||||
}
|
|
||||||
|
|
||||||
func countCodeLines(lines []asmLine) int {
|
|
||||||
n := 0
|
|
||||||
for _, l := range lines {
|
|
||||||
if l.isCode {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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)
|
|
||||||
original := parsed
|
|
||||||
|
|
||||||
if cfg.EnableStoreLoad {
|
|
||||||
parsed = passStoreReload(parsed, cfg)
|
|
||||||
}
|
|
||||||
if cfg.EnableLoad {
|
|
||||||
parsed = passLoadElimination(parsed, cfg)
|
|
||||||
}
|
|
||||||
if cfg.EnableImm {
|
|
||||||
parsed = passImmElimination(parsed)
|
|
||||||
}
|
|
||||||
if cfg.EnableJmp {
|
|
||||||
parsed = passJmpNext(parsed)
|
|
||||||
}
|
|
||||||
if cfg.EnableSelf {
|
|
||||||
parsed = passSelfAssignment(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = stripOptMarkers(parsed)
|
|
||||||
if cfg.Debug {
|
|
||||||
original = stripOptMarkers(original)
|
|
||||||
parsed = debugDiff(original, parsed)
|
|
||||||
}
|
|
||||||
return linesToString(parsed)
|
|
||||||
}
|
|
||||||
|
|
@ -1,518 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
package optimizer
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// 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.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 {
|
|
||||||
if !isImmediate(line.operand) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val := parseValueAfterHash(line.operand)
|
|
||||||
if val < 0 {
|
|
||||||
return false // unparseable (label ref, <label, >label)
|
|
||||||
}
|
|
||||||
switch line.opcode {
|
|
||||||
case "lda":
|
|
||||||
if rs.a.val == val {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
rs.loadImm("a", val)
|
|
||||||
case "ldx":
|
|
||||||
if rs.x.val == val {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
rs.loadImm("x", val)
|
|
||||||
case "ldy":
|
|
||||||
if rs.y.val == val {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
rs.loadImm("y", val)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseValueAfterHash extracts the numeric value after '#'. Returns -1 for unparseable.
|
|
||||||
func parseValueAfterHash(operand string) int {
|
|
||||||
if len(operand) < 2 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
s := operand[1:]
|
|
||||||
// ACME high/low byte operators are not evaluable at optimize time
|
|
||||||
if strings.HasPrefix(s, "<") || strings.HasPrefix(s, ">") {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return parseHexOrDec(s)
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
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]
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
package optimizer
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// 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, cfg *Config) []asmLine {
|
|
||||||
rs := newRegState()
|
|
||||||
var result []asmLine
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
if !line.isCode {
|
|
||||||
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 isIndexedOperand(line.operand) {
|
|
||||||
updateRegState(line, rs)
|
|
||||||
} else if isIOAddr(line.operand, cfg) {
|
|
||||||
// I/O register reads may return different values — don't track
|
|
||||||
rs.resetA()
|
|
||||||
updateRegState(line, rs)
|
|
||||||
} else if rs.a.src == line.operand {
|
|
||||||
continue // redundant, remove
|
|
||||||
} else {
|
|
||||||
rs.a.src = line.operand
|
|
||||||
}
|
|
||||||
} else if line.opcode == "ldx" && !isImmediate(line.operand) {
|
|
||||||
if isIndexedOperand(line.operand) {
|
|
||||||
updateRegState(line, rs)
|
|
||||||
} else if isIOAddr(line.operand, cfg) {
|
|
||||||
rs.resetX()
|
|
||||||
updateRegState(line, rs)
|
|
||||||
} else if rs.x.src == line.operand {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
rs.x.src = line.operand
|
|
||||||
}
|
|
||||||
} else if line.opcode == "ldy" && !isImmediate(line.operand) {
|
|
||||||
if isIndexedOperand(line.operand) {
|
|
||||||
updateRegState(line, rs)
|
|
||||||
} else if isIOAddr(line.operand, cfg) {
|
|
||||||
rs.resetY()
|
|
||||||
updateRegState(line, rs)
|
|
||||||
} else if rs.y.src == line.operand {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
rs.y.src = line.operand
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateRegState(line, rs)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// isIndexedOperand returns true for addressing modes where the effective address
|
|
||||||
// depends on X or Y registers (e.g. (zp),y, $xxxx,x, zp,x).
|
|
||||||
// Such loads can't be tracked by operand text alone since the register may change.
|
|
||||||
func isIndexedOperand(operand string) bool {
|
|
||||||
return strings.Contains(operand, "(") || strings.Contains(operand, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// isIOAddr returns true if operand is a hex address marked as I/O in the config.
|
|
||||||
func isIOAddr(operand string, cfg *Config) bool {
|
|
||||||
if cfg == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(operand, "$") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
addr := parseHexOrDec(operand)
|
|
||||||
return addr >= 0 && addr < 65536 && cfg.IOMap[addr]
|
|
||||||
}
|
|
||||||
|
|
||||||
func isImmediate(operand string) bool {
|
|
||||||
return len(operand) > 0 && operand[0] == '#'
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
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]
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
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, "#") {
|
|
||||||
if val := parseHexOrDec(line.operand[1:]); val >= 0 {
|
|
||||||
rs.loadImm("a", val)
|
|
||||||
} else {
|
|
||||||
rs.resetA()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rs.loadMem("a", line.operand)
|
|
||||||
}
|
|
||||||
case "ldx":
|
|
||||||
if strings.HasPrefix(line.operand, "#") {
|
|
||||||
if val := parseHexOrDec(line.operand[1:]); val >= 0 {
|
|
||||||
rs.loadImm("x", val)
|
|
||||||
} else {
|
|
||||||
rs.resetX()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rs.loadMem("x", line.operand)
|
|
||||||
}
|
|
||||||
case "ldy":
|
|
||||||
if strings.HasPrefix(line.operand, "#") {
|
|
||||||
if val := parseHexOrDec(line.operand[1:]); val >= 0 {
|
|
||||||
rs.loadImm("y", val)
|
|
||||||
} else {
|
|
||||||
rs.resetY()
|
|
||||||
}
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
package optimizer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseHexOrDec parses an ACME-style number: $ff or 255.
|
|
||||||
// Returns -1 if s is not a parseable simple number (e.g. label references, <label, >label).
|
|
||||||
func parseHexOrDec(s string) int {
|
|
||||||
if strings.HasPrefix(s, "$") {
|
|
||||||
v, err := strconv.ParseUint(s[1:], 16, 16)
|
|
||||||
if err != nil {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
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, err := strconv.ParseUint(s, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return int(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSimpleNumber returns true if s is a plain hex/dec number (no label refs, <, >, etc.)
|
|
||||||
func isSimpleNumber(s string) bool {
|
|
||||||
return parseHexOrDec(s) >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
54
main.go
54
main.go
|
|
@ -13,7 +13,6 @@ import (
|
||||||
|
|
||||||
"c65gm/internal/commands"
|
"c65gm/internal/commands"
|
||||||
"c65gm/internal/compiler"
|
"c65gm/internal/compiler"
|
||||||
"c65gm/internal/optimizer"
|
|
||||||
"c65gm/internal/preproc"
|
"c65gm/internal/preproc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -115,20 +114,20 @@ func main() {
|
||||||
// Determine mode by output extension
|
// Determine mode by output extension
|
||||||
if strings.HasSuffix(strings.ToLower(outputFile), ".prg") {
|
if strings.HasSuffix(strings.ToLower(outputFile), ".prg") {
|
||||||
// Build mode (compile + assemble)
|
// Build mode (compile + assemble)
|
||||||
if err := build(inputFile, outputFile, false, false, false, false, false, nil); err != nil {
|
if err := build(inputFile, outputFile, false, false); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Build successful.")
|
fmt.Println("Build successful.")
|
||||||
} else {
|
} else {
|
||||||
// Compile mode (assembly only)
|
// Compile mode (assembly only)
|
||||||
if err := compileOnly(inputFile, outputFile, false, false, false, nil); err != nil {
|
if err := compileOnly(inputFile, outputFile); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Compilation successful.")
|
fmt.Println("Compilation successful.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileOnly(inFile, outFile string, opt, optDebug, optC64 bool, optExcludes []string) error {
|
func compileOnly(inFile, outFile string) error {
|
||||||
// Preprocess
|
// Preprocess
|
||||||
lines, pragma, err := preproc.PreProcess(inFile)
|
lines, pragma, err := preproc.PreProcess(inFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -139,20 +138,6 @@ func compileOnly(inFile, outFile string, opt, optDebug, optC64 bool, optExcludes
|
||||||
|
|
||||||
// Create compiler and register commands
|
// Create compiler and register commands
|
||||||
comp := compiler.NewCompiler(pragma)
|
comp := compiler.NewCompiler(pragma)
|
||||||
comp.CmdlineOpt = opt
|
|
||||||
comp.CmdlineDebug = optDebug
|
|
||||||
// Build I/O regions from CLI flags
|
|
||||||
if optC64 {
|
|
||||||
comp.CmdlineIORegions = append(comp.CmdlineIORegions, optimizer.IORegion{Start: 0xD000, End: 0xDFFF})
|
|
||||||
}
|
|
||||||
for _, raw := range optExcludes {
|
|
||||||
region, err := optimizer.ParseExcludeRange(raw)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: --opt-exclude: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
comp.CmdlineIORegions = append(comp.CmdlineIORegions, region)
|
|
||||||
}
|
|
||||||
registerCommands(comp)
|
registerCommands(comp)
|
||||||
|
|
||||||
// Compile
|
// Compile
|
||||||
|
|
@ -248,32 +233,12 @@ func printUsage() {
|
||||||
fmt.Println(" -V, --version Show version")
|
fmt.Println(" -V, --version Show version")
|
||||||
}
|
}
|
||||||
|
|
||||||
// excludeFlag is a repeatable flag value for --opt-exclude
|
|
||||||
type excludeFlag []string
|
|
||||||
|
|
||||||
func (e *excludeFlag) String() string {
|
|
||||||
if len(*e) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.Join(*e, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *excludeFlag) Set(value string) error {
|
|
||||||
*e = append(*e, value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runBuildCommand(args []string) {
|
func runBuildCommand(args []string) {
|
||||||
buildCmd := flag.NewFlagSet("build", flag.ExitOnError)
|
buildCmd := flag.NewFlagSet("build", flag.ExitOnError)
|
||||||
input := buildCmd.String("i", "", "input .c65 file (required)")
|
input := buildCmd.String("i", "", "input .c65 file (required)")
|
||||||
output := buildCmd.String("o", "", "output .prg file (default: <input>.prg)")
|
output := buildCmd.String("o", "", "output .prg file (default: <input>.prg)")
|
||||||
keepAsm := buildCmd.Bool("keep-asm", false, "keep intermediate assembly file")
|
keepAsm := buildCmd.Bool("keep-asm", false, "keep intermediate assembly file")
|
||||||
noCBM := buildCmd.Bool("no-cbm", false, "don't add -f cbm flag to ACME")
|
noCBM := buildCmd.Bool("no-cbm", false, "don't add -f cbm flag to ACME")
|
||||||
opt := buildCmd.Bool("opt", false, "enable all optimizations")
|
|
||||||
optDebug := buildCmd.Bool("opt-debug", false, "show optimization changes in output")
|
|
||||||
optC64 := buildCmd.Bool("opt-exclude-c64-io", false, "exclude C64 I/O region ($D000-$DFFF) from optimization")
|
|
||||||
var optExcludes excludeFlag
|
|
||||||
buildCmd.Var(&optExcludes, "opt-exclude", "exclude address range from optimization (repeatable, format: START:END)")
|
|
||||||
|
|
||||||
if err := buildCmd.Parse(args); err != nil {
|
if err := buildCmd.Parse(args); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
||||||
|
|
@ -297,7 +262,7 @@ func runBuildCommand(args []string) {
|
||||||
*output = *output + ".prg"
|
*output = *output + ".prg"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := build(*input, *output, *keepAsm, *noCBM, *opt, *optDebug, *optC64, []string(optExcludes)); err != nil {
|
if err := build(*input, *output, *keepAsm, *noCBM); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Build successful.")
|
fmt.Println("Build successful.")
|
||||||
|
|
@ -307,11 +272,6 @@ func runCompileCommand(args []string) {
|
||||||
compileCmd := flag.NewFlagSet("compile", flag.ExitOnError)
|
compileCmd := flag.NewFlagSet("compile", flag.ExitOnError)
|
||||||
input := compileCmd.String("i", "", "input .c65 file (required)")
|
input := compileCmd.String("i", "", "input .c65 file (required)")
|
||||||
output := compileCmd.String("o", "", "output .asm file (default: <input>.asm)")
|
output := compileCmd.String("o", "", "output .asm file (default: <input>.asm)")
|
||||||
opt := compileCmd.Bool("opt", false, "enable all optimizations")
|
|
||||||
optDebug := compileCmd.Bool("opt-debug", false, "show optimization changes in output")
|
|
||||||
optC64 := compileCmd.Bool("opt-exclude-c64-io", false, "exclude C64 I/O region ($D000-$DFFF) from optimization")
|
|
||||||
var optExcludes excludeFlag
|
|
||||||
compileCmd.Var(&optExcludes, "opt-exclude", "exclude address range from optimization (repeatable, format: START:END)")
|
|
||||||
|
|
||||||
if err := compileCmd.Parse(args); err != nil {
|
if err := compileCmd.Parse(args); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
||||||
|
|
@ -335,13 +295,13 @@ func runCompileCommand(args []string) {
|
||||||
*output = *output + ".asm"
|
*output = *output + ".asm"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compileOnly(*input, *output, *opt, *optDebug, *optC64, []string(optExcludes)); err != nil {
|
if err := compileOnly(*input, *output); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Compilation successful.")
|
fmt.Println("Compilation successful.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func build(inputFile, outputFile string, keepAsm, noCBM, opt, optDebug, optC64 bool, optExcludes []string) error {
|
func build(inputFile, outputFile string, keepAsm, noCBM bool) error {
|
||||||
// Check if ACME is available before starting compilation
|
// Check if ACME is available before starting compilation
|
||||||
if err := checkACMEAvailable(); err != nil {
|
if err := checkACMEAvailable(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -352,7 +312,7 @@ func build(inputFile, outputFile string, keepAsm, noCBM, opt, optDebug, optC64 b
|
||||||
asmFile := base + ".asm"
|
asmFile := base + ".asm"
|
||||||
|
|
||||||
// Compile to assembly
|
// Compile to assembly
|
||||||
if err := compileOnly(inputFile, asmFile, opt, optDebug, optC64, optExcludes); err != nil {
|
if err := compileOnly(inputFile, asmFile); err != nil {
|
||||||
return fmt.Errorf("compilation failed: %w", err)
|
return fmt.Errorf("compilation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue