Add experimental peephole optimizer with 5 passes, CLI flags, pragmas, and I/O region protection.
This commit is contained in:
parent
28723878b1
commit
1abad8bb92
9 changed files with 580 additions and 50 deletions
109
README.md
109
README.md
|
|
@ -201,6 +201,115 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ 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
|
||||||
|
|
@ -281,12 +284,18 @@ 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
|
// isOptimizing returns true if any peephole optimization pragma is active
|
||||||
func (c *Compiler) isOptimizing() bool {
|
func (c *Compiler) isOptimizing() bool {
|
||||||
|
if c.CmdlineOpt {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if c.ctx == nil || c.ctx.Pragma == nil {
|
if c.ctx == nil || c.ctx.Pragma == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -314,13 +323,65 @@ func (c *Compiler) getOptimizerConfig() *optimizer.Config {
|
||||||
idx := c.ctx.Pragma.GetCurrentPragmaSetIndex()
|
idx := c.ctx.Pragma.GetCurrentPragmaSetIndex()
|
||||||
ps := c.ctx.Pragma.GetPragmaSetByIndex(idx)
|
ps := c.ctx.Pragma.GetPragmaSetByIndex(idx)
|
||||||
cfg := optimizer.NewConfig(ps)
|
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() {
|
if !cfg.Any() {
|
||||||
return nil
|
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)
|
cfg.BuildIOMap(c.ctx.Pragma)
|
||||||
return cfg
|
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 {
|
func classString(cls CmdClass) string {
|
||||||
switch cls {
|
switch cls {
|
||||||
case CmdLinear:
|
case CmdLinear:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package optimizer
|
package optimizer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -70,3 +71,57 @@ func parsePragmaAddr(s string) int {
|
||||||
}
|
}
|
||||||
return int(v)
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,132 @@ func TestNewConfigWithAll(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestNewConfigPragmasAccumulate(t *testing.T) {
|
||||||
// Pragma sets COPY previous values — so last set has ALL pragmas
|
// Pragma sets COPY previous values — so last set has ALL pragmas
|
||||||
p := preproc.NewPragma()
|
p := preproc.NewPragma()
|
||||||
|
|
|
||||||
|
|
@ -5,45 +5,46 @@ import "fmt"
|
||||||
// debugDiff produces annotated output showing original vs optimized code.
|
// debugDiff produces annotated output showing original vs optimized code.
|
||||||
// Lines that were removed are shown with "[removed]" annotation.
|
// Lines that were removed are shown with "[removed]" annotation.
|
||||||
func debugDiff(original, optimized []asmLine) []asmLine {
|
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)
|
origCount := countCodeLines(original)
|
||||||
optCount := countCodeLines(optimized)
|
|
||||||
saved := origCount - optCount
|
|
||||||
|
|
||||||
header := fmt.Sprintf("; --- peephole: %d asm lines → %d (%d saved) ---", origCount, optCount, saved)
|
var body []asmLine
|
||||||
var result []asmLine
|
oi := 0
|
||||||
result = append(result, asmLine{text: header, isComment: true})
|
opi := 0
|
||||||
|
removedCount := 0
|
||||||
// Walk both arrays in sync. When optimized doesn't have a line
|
|
||||||
// that original has, it was removed.
|
|
||||||
oi := 0 // index into original (pre-stripped)
|
|
||||||
opi := 0 // index into optimized
|
|
||||||
|
|
||||||
for oi < len(original) {
|
for oi < len(original) {
|
||||||
origLine := original[oi]
|
origLine := original[oi]
|
||||||
|
|
||||||
if opi < len(optimized) && linesMatch(origLine, optimized[opi]) {
|
if opi < len(optimized) && linesMatch(origLine, optimized[opi]) {
|
||||||
result = append(result, optimized[opi])
|
body = append(body, optimized[opi])
|
||||||
oi++
|
oi++
|
||||||
opi++
|
opi++
|
||||||
} else if !origLine.isCode {
|
} else if !origLine.isCode {
|
||||||
// Non-code line that doesn't match anything — pass through
|
body = append(body, origLine)
|
||||||
result = append(result, origLine)
|
|
||||||
oi++
|
oi++
|
||||||
} else {
|
} else {
|
||||||
// Code line in original but not in optimized → removed
|
body = append(body, asmLine{
|
||||||
result = append(result, asmLine{
|
|
||||||
text: fmt.Sprintf("; [removed] %s", origLine.text),
|
text: fmt.Sprintf("; [removed] %s", origLine.text),
|
||||||
isComment: true,
|
isComment: true,
|
||||||
})
|
})
|
||||||
|
removedCount++
|
||||||
oi++
|
oi++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append any remaining optimized lines (shouldn't happen in practice)
|
|
||||||
for ; opi < len(optimized); opi++ {
|
for ; opi < len(optimized); opi++ {
|
||||||
result = append(result, 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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ func Optimize(lines []string, cfg *Config) []string {
|
||||||
parsed = passStoreReload(parsed, cfg)
|
parsed = passStoreReload(parsed, cfg)
|
||||||
}
|
}
|
||||||
if cfg.EnableLoad {
|
if cfg.EnableLoad {
|
||||||
parsed = passLoadElimination(parsed)
|
parsed = passLoadElimination(parsed, cfg)
|
||||||
}
|
}
|
||||||
if cfg.EnableImm {
|
if cfg.EnableImm {
|
||||||
parsed = passImmElimination(parsed)
|
parsed = passImmElimination(parsed)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ func TestPassLoadElimination(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
parsed := parseLines(tt.input)
|
parsed := parseLines(tt.input)
|
||||||
result := passLoadElimination(parsed)
|
result := passLoadElimination(parsed, nil)
|
||||||
cleaned := stripOptMarkers(result)
|
cleaned := stripOptMarkers(result)
|
||||||
if got := len(cleaned); got != tt.expected {
|
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))
|
t.Errorf("got %d lines, want %d\ninput: %v\noutput: %v", got, tt.expected, tt.input, linesToString(cleaned))
|
||||||
|
|
@ -173,6 +173,38 @@ func TestPassSelfAssignment(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestPassLoadNoCommentReset(t *testing.T) {
|
||||||
// Comments should NOT reset register state
|
// Comments should NOT reset register state
|
||||||
parsed := parseLines(lines(
|
parsed := parseLines(lines(
|
||||||
|
|
@ -181,7 +213,7 @@ func TestPassLoadNoCommentReset(t *testing.T) {
|
||||||
"; comment",
|
"; comment",
|
||||||
"\tlda b", // should be redundant
|
"\tlda b", // should be redundant
|
||||||
))
|
))
|
||||||
result := passLoadElimination(parsed)
|
result := passLoadElimination(parsed, nil)
|
||||||
if len(result) != 3 {
|
if len(result) != 3 {
|
||||||
t.Errorf("expected 3 lines (comment kept, lda b removed), got %d:\n%v", len(result), linesToString(result))
|
t.Errorf("expected 3 lines (comment kept, lda b removed), got %d:\n%v", len(result), linesToString(result))
|
||||||
}
|
}
|
||||||
|
|
@ -389,13 +421,98 @@ func TestPassStoreReload(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPassStoreReloadIO(t *testing.T) {
|
func TestPassStoreReloadIO(t *testing.T) {
|
||||||
|
t.Run("c64 border color", func(t *testing.T) {
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
cfg.IOMap[0xD020] = true
|
cfg.IOMap[0xD020] = true
|
||||||
|
|
||||||
parsed := parseLines(lines("\tsta $D020", "\tlda $D020"))
|
parsed := parseLines(lines("\tsta $D020", "\tlda $D020"))
|
||||||
result := passStoreReload(parsed, cfg)
|
result := passStoreReload(parsed, cfg)
|
||||||
cleaned := stripOptMarkers(result)
|
cleaned := stripOptMarkers(result)
|
||||||
if len(cleaned) != 2 {
|
if len(cleaned) != 2 {
|
||||||
t.Errorf("expected 2 lines (I/O skip), got %d", len(cleaned))
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import "strings"
|
||||||
// passLoadElimination removes redundant memory-to-register loads.
|
// passLoadElimination removes redundant memory-to-register loads.
|
||||||
// Pattern: lda X; ... (X not modified) ...; lda X → remove second
|
// Pattern: lda X; ... (X not modified) ...; lda X → remove second
|
||||||
// Applies to A, X, Y registers identically.
|
// Applies to A, X, Y registers identically.
|
||||||
func passLoadElimination(lines []asmLine) []asmLine {
|
func passLoadElimination(lines []asmLine, cfg *Config) []asmLine {
|
||||||
rs := newRegState()
|
rs := newRegState()
|
||||||
var result []asmLine
|
var result []asmLine
|
||||||
|
|
||||||
|
|
@ -27,7 +27,10 @@ func passLoadElimination(lines []asmLine) []asmLine {
|
||||||
// Check for redundant memory load
|
// Check for redundant memory load
|
||||||
if line.opcode == "lda" && !isImmediate(line.operand) {
|
if line.opcode == "lda" && !isImmediate(line.operand) {
|
||||||
if isIndexedOperand(line.operand) {
|
if isIndexedOperand(line.operand) {
|
||||||
// Indexed/indirect addressing: address depends on X/Y, can't track by operand text alone
|
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)
|
updateRegState(line, rs)
|
||||||
} else if rs.a.src == line.operand {
|
} else if rs.a.src == line.operand {
|
||||||
continue // redundant, remove
|
continue // redundant, remove
|
||||||
|
|
@ -37,6 +40,9 @@ func passLoadElimination(lines []asmLine) []asmLine {
|
||||||
} else if line.opcode == "ldx" && !isImmediate(line.operand) {
|
} else if line.opcode == "ldx" && !isImmediate(line.operand) {
|
||||||
if isIndexedOperand(line.operand) {
|
if isIndexedOperand(line.operand) {
|
||||||
updateRegState(line, rs)
|
updateRegState(line, rs)
|
||||||
|
} else if isIOAddr(line.operand, cfg) {
|
||||||
|
rs.resetX()
|
||||||
|
updateRegState(line, rs)
|
||||||
} else if rs.x.src == line.operand {
|
} else if rs.x.src == line.operand {
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -45,6 +51,9 @@ func passLoadElimination(lines []asmLine) []asmLine {
|
||||||
} else if line.opcode == "ldy" && !isImmediate(line.operand) {
|
} else if line.opcode == "ldy" && !isImmediate(line.operand) {
|
||||||
if isIndexedOperand(line.operand) {
|
if isIndexedOperand(line.operand) {
|
||||||
updateRegState(line, rs)
|
updateRegState(line, rs)
|
||||||
|
} else if isIOAddr(line.operand, cfg) {
|
||||||
|
rs.resetY()
|
||||||
|
updateRegState(line, rs)
|
||||||
} else if rs.y.src == line.operand {
|
} else if rs.y.src == line.operand {
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -67,6 +76,18 @@ func isIndexedOperand(operand string) bool {
|
||||||
return strings.Contains(operand, "(") || strings.Contains(operand, ",")
|
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 {
|
func isImmediate(operand string) bool {
|
||||||
return len(operand) > 0 && operand[0] == '#'
|
return len(operand) > 0 && operand[0] == '#'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
main.go
54
main.go
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"c65gm/internal/commands"
|
"c65gm/internal/commands"
|
||||||
"c65gm/internal/compiler"
|
"c65gm/internal/compiler"
|
||||||
|
"c65gm/internal/optimizer"
|
||||||
"c65gm/internal/preproc"
|
"c65gm/internal/preproc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -114,20 +115,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); err != nil {
|
if err := build(inputFile, outputFile, false, false, false, false, false, nil); 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); err != nil {
|
if err := compileOnly(inputFile, outputFile, false, false, false, nil); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Compilation successful.")
|
fmt.Println("Compilation successful.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileOnly(inFile, outFile string) error {
|
func compileOnly(inFile, outFile string, opt, optDebug, optC64 bool, optExcludes []string) error {
|
||||||
// Preprocess
|
// Preprocess
|
||||||
lines, pragma, err := preproc.PreProcess(inFile)
|
lines, pragma, err := preproc.PreProcess(inFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -138,6 +139,20 @@ func compileOnly(inFile, outFile string) error {
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -233,12 +248,32 @@ 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)
|
||||||
|
|
@ -262,7 +297,7 @@ func runBuildCommand(args []string) {
|
||||||
*output = *output + ".prg"
|
*output = *output + ".prg"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := build(*input, *output, *keepAsm, *noCBM); err != nil {
|
if err := build(*input, *output, *keepAsm, *noCBM, *opt, *optDebug, *optC64, []string(optExcludes)); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Build successful.")
|
fmt.Println("Build successful.")
|
||||||
|
|
@ -272,6 +307,11 @@ 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)
|
||||||
|
|
@ -295,13 +335,13 @@ func runCompileCommand(args []string) {
|
||||||
*output = *output + ".asm"
|
*output = *output + ".asm"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compileOnly(*input, *output); err != nil {
|
if err := compileOnly(*input, *output, *opt, *optDebug, *optC64, []string(optExcludes)); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Compilation successful.")
|
fmt.Println("Compilation successful.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func build(inputFile, outputFile string, keepAsm, noCBM bool) error {
|
func build(inputFile, outputFile string, keepAsm, noCBM, opt, optDebug, optC64 bool, optExcludes []string) 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
|
||||||
|
|
@ -312,7 +352,7 @@ func build(inputFile, outputFile string, keepAsm, noCBM bool) error {
|
||||||
asmFile := base + ".asm"
|
asmFile := base + ".asm"
|
||||||
|
|
||||||
// Compile to assembly
|
// Compile to assembly
|
||||||
if err := compileOnly(inputFile, asmFile); err != nil {
|
if err := compileOnly(inputFile, asmFile, opt, optDebug, optC64, optExcludes); err != nil {
|
||||||
return fmt.Errorf("compilation failed: %w", err)
|
return fmt.Errorf("compilation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue