Improved peephole optimizer. Sending an output asm format to optimizer with kept info about asm source (generated, script or asm block)

This commit is contained in:
Mattias Hansson 2026-08-21 21:42:55 +02:00
parent 7d5d0ac7f3
commit 97c976c218
10 changed files with 576 additions and 179 deletions

View file

@ -308,7 +308,7 @@ The optimizer never removes **reads or stores** to addresses marked in the I/O e
2. **CLI shorthand**: `--opt-exclude-c64-io` for the full C64 I/O page 2. **CLI shorthand**: `--opt-exclude-c64-io` for the full C64 I/O page
3. **Pragma**: `#PRAGMA _P_OPT_IO $D000 $DFFF` 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. Variable names (like `vic2`, `BORDER_COLOR`) are NOT checked against the I/O map — only literal numeric addresses (hex `$D020` or decimal `53280`) 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

View file

@ -14,11 +14,11 @@ import (
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 []optimizer.SourceLine // ASM blocks with _P_ASM_AFTER_VARS pragma
dissolvedVars map[string]bool // REGISTER vars dissolved by optimizer dissolvedVars map[string]bool // REGISTER vars dissolved by optimizer
CmdlineOpt bool // --opt enables all passes CmdlineOpt bool // --opt enables all passes
CmdlineDebug bool // --opt-debug enables debug output CmdlineDebug bool // --opt-debug enables debug output
CmdlineIORegions []optimizer.IORegion // --opt-exclude ranges 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
@ -39,9 +39,51 @@ func (c *Compiler) Registry() *CommandRegistry {
return c.registry return c.registry
} }
// generatedLine tags a line as compiler-generated (optimizable).
func generatedLine(text string) optimizer.SourceLine {
return optimizer.SourceLine{Text: text, Origin: optimizer.OriginGenerated}
}
// generatedLines tags a batch of compiler-generated lines.
func generatedLines(lines []string) []optimizer.SourceLine {
out := make([]optimizer.SourceLine, len(lines))
for i, l := range lines {
out[i] = generatedLine(l)
}
return out
}
// asmSourceLine tags a line as verbatim ASM block content.
func asmSourceLine(text string) optimizer.SourceLine {
return optimizer.SourceLine{Text: text, Origin: optimizer.OriginAsm}
}
// scriptSourceLine tags a line as verbatim SCRIPT print() output.
func scriptSourceLine(text string) optimizer.SourceLine {
return optimizer.SourceLine{Text: text, Origin: optimizer.OriginScript}
}
// scriptSourceLines tags a batch of SCRIPT print() output lines.
func scriptSourceLines(lines []string) []optimizer.SourceLine {
out := make([]optimizer.SourceLine, len(lines))
for i, l := range lines {
out[i] = scriptSourceLine(l)
}
return out
}
// asmSourceLines tags a batch of verbatim ASM block lines.
func asmSourceLines(lines []string) []optimizer.SourceLine {
out := make([]optimizer.SourceLine, len(lines))
for i, l := range lines {
out[i] = asmSourceLine(l)
}
return out
}
// Compile processes preprocessed lines and generates assembly output // Compile processes preprocessed lines and generates assembly output
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
var codeOutput []string var codeOutput []optimizer.SourceLine
var lastKind = preproc.Source var lastKind = preproc.Source
var scriptBuffer []preproc.Line var scriptBuffer []preproc.Line
var scriptIsLibrary bool var scriptIsLibrary bool
@ -50,7 +92,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
var currentMacroParams []string var currentMacroParams []string
var currentMacroSourceFile string var currentMacroSourceFile string
var currentMacroStartLine int var currentMacroStartLine int
var currentAsmTarget *[]string // nil = no active ASM block, or points to target slice var currentAsmTarget *[]optimizer.SourceLine // nil = no active ASM block, or points to target slice
// Reset deferred ASM storage for this compilation // Reset deferred ASM storage for this compilation
c.deferredAsm = nil c.deferredAsm = nil
@ -64,12 +106,12 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("script execution failed: %w", err) return nil, fmt.Errorf("script execution failed: %w", err)
} }
codeOutput = append(codeOutput, scriptOutput...) codeOutput = append(codeOutput, scriptSourceLines(scriptOutput)...)
scriptBuffer = nil scriptBuffer = nil
if scriptIsLibrary { if scriptIsLibrary {
codeOutput = append(codeOutput, "; ENDSCRIPT LIBRARY") codeOutput = append(codeOutput, generatedLine("; ENDSCRIPT LIBRARY"))
} else { } else {
codeOutput = append(codeOutput, "; ENDSCRIPT") codeOutput = append(codeOutput, generatedLine("; ENDSCRIPT"))
} }
} }
@ -83,7 +125,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
SourceFile: currentMacroSourceFile, SourceFile: currentMacroSourceFile,
StartLine: currentMacroStartLine, StartLine: currentMacroStartLine,
} }
codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName)) codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName)))
} }
macroBuffer = nil macroBuffer = nil
currentMacroName = "" currentMacroName = ""
@ -94,7 +136,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Close previous Assembler block // Close previous Assembler block
if lastKind == preproc.Assembler && currentAsmTarget != nil { if lastKind == preproc.Assembler && currentAsmTarget != nil {
*currentAsmTarget = append(*currentAsmTarget, "; ENDASM") *currentAsmTarget = append(*currentAsmTarget, asmSourceLine("; ENDASM"))
currentAsmTarget = nil currentAsmTarget = nil
} }
@ -103,24 +145,24 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Check if ASM block should be deferred to end // Check if ASM block should be deferred to end
pragmaSet := c.ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) pragmaSet := c.ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
asmAfterVars := pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "" && asmAfterVars := pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "" &&
pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "0" pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "0"
if asmAfterVars { if asmAfterVars {
// Add inline comment and defer ASM block // Add inline comment and defer ASM block
codeOutput = append(codeOutput, "; ASM block deferred to end of source") codeOutput = append(codeOutput, generatedLine("; ASM block deferred to end of source"))
c.deferredAsm = append(c.deferredAsm, c.deferredAsm = append(c.deferredAsm,
fmt.Sprintf("; ASM Block from %s, Line %d", line.Filename, line.LineNo)) asmSourceLine(fmt.Sprintf("; ASM Block from %s, Line %d", line.Filename, line.LineNo)))
currentAsmTarget = &c.deferredAsm currentAsmTarget = &c.deferredAsm
} else { } else {
// Normal ASM block // Normal ASM block
codeOutput = append(codeOutput, "; ASM") codeOutput = append(codeOutput, generatedLine("; ASM"))
currentAsmTarget = &codeOutput currentAsmTarget = &codeOutput
} }
} else if line.Kind == preproc.Script { } else if line.Kind == preproc.Script {
codeOutput = append(codeOutput, "; SCRIPT") codeOutput = append(codeOutput, generatedLine("; SCRIPT"))
scriptIsLibrary = false scriptIsLibrary = false
} else if line.Kind == preproc.ScriptLibrary { } else if line.Kind == preproc.ScriptLibrary {
codeOutput = append(codeOutput, "; SCRIPT LIBRARY") codeOutput = append(codeOutput, generatedLine("; SCRIPT LIBRARY"))
scriptIsLibrary = true scriptIsLibrary = true
} else if line.Kind == preproc.ScriptMacroDef { } else if line.Kind == preproc.ScriptMacroDef {
// First line is the header - parse it // First line is the header - parse it
@ -131,7 +173,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} }
currentMacroName = name currentMacroName = name
currentMacroParams = params currentMacroParams = params
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text)) codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; %s", line.Text)))
} }
lastKind = line.Kind lastKind = line.Kind
@ -177,9 +219,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} }
// Emit with comments showing invocation // Emit with comments showing invocation
*currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; %s", text)) *currentAsmTarget = append(*currentAsmTarget, asmSourceLine(fmt.Sprintf("; %s", text)))
*currentAsmTarget = append(*currentAsmTarget, macroOutput...) *currentAsmTarget = append(*currentAsmTarget, asmSourceLines(macroOutput)...)
*currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; end @%s", macroName)) *currentAsmTarget = append(*currentAsmTarget, asmSourceLine(fmt.Sprintf("; end @%s", macroName)))
continue continue
} }
} }
@ -208,7 +250,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Continue searching after the replacement // Continue searching after the replacement
searchFrom = start + len(expandedName) searchFrom = start + len(expandedName)
} }
*currentAsmTarget = append(*currentAsmTarget, codePart+commentPart) *currentAsmTarget = append(*currentAsmTarget, asmSourceLine(codePart+commentPart))
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary { } else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
// Collect script lines for execution // Collect script lines for execution
scriptBuffer = append(scriptBuffer, line) scriptBuffer = append(scriptBuffer, line)
@ -253,18 +295,18 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
return nil, fmt.Errorf("compilation failed") return nil, fmt.Errorf("compilation failed")
} }
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text)) codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; %s", line.Text)))
if len(asmLines) > 0 && c.isMarkersEnabled() { if len(asmLines) > 0 && c.isMarkersEnabled() {
codeOutput = append(codeOutput, fmt.Sprintf("; @@OPT:%s:%s", classString(cmd.GetClass()), cmd.GetName())) codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; @@OPT:%s:%s", classString(cmd.GetClass()), cmd.GetName())))
} }
codeOutput = append(codeOutput, asmLines...) codeOutput = append(codeOutput, generatedLines(asmLines)...)
} }
// Close any open block // Close any open block
if lastKind == preproc.Assembler { if lastKind == preproc.Assembler {
// Close the final ASM block if still open // Close the final ASM block if still open
if currentAsmTarget != nil { if currentAsmTarget != nil {
*currentAsmTarget = append(*currentAsmTarget, "; ENDASM") *currentAsmTarget = append(*currentAsmTarget, asmSourceLine("; ENDASM"))
} }
return nil, fmt.Errorf("Unclosed ASM block.") return nil, fmt.Errorf("Unclosed ASM block.")
} else if lastKind == preproc.Script { } else if lastKind == preproc.Script {
@ -276,10 +318,13 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} }
// Peephole optimization pass // Peephole optimization pass
var codeStrings []string
if cfg := c.getOptimizerConfig(); cfg != nil { if cfg := c.getOptimizerConfig(); cfg != nil {
var dissolved map[string]bool var dissolved map[string]bool
codeOutput, dissolved = optimizer.Optimize(codeOutput, cfg) codeStrings, dissolved = optimizer.Optimize(codeOutput, cfg)
c.dissolvedVars = dissolved c.dissolvedVars = dissolved
} else {
codeStrings = optimizer.SourceLineTexts(codeOutput)
} }
// Analyze for overlapping absolute addresses in function call chains // Analyze for overlapping absolute addresses in function call chains
@ -301,13 +346,13 @@ 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) codeStrings, removedFuncs := c.removeUnusedFunctions(codeStrings)
// Update peephole header to match actual [removed] count after function removal // Update peephole header to match actual [removed] count after function removal
codeOutput = updatePeepholeHeader(codeOutput) codeStrings = updatePeepholeHeader(codeStrings)
// Assemble final output with headers and footers // Assemble final output with headers and footers
return c.assembleOutput(codeOutput, removedFuncs), nil return c.assembleOutput(codeStrings, removedFuncs), nil
} }
// isOptimizing returns true if any peephole optimization pragma is active // isOptimizing returns true if any peephole optimization pragma is active
@ -757,7 +802,7 @@ func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bo
if len(c.deferredAsm) > 0 { if len(c.deferredAsm) > 0 {
output = append(output, "; Deferred ASM blocks (after variables)") output = append(output, "; Deferred ASM blocks (after variables)")
output = append(output, "") output = append(output, "")
output = append(output, c.deferredAsm...) output = append(output, optimizer.SourceLineTexts(c.deferredAsm)...)
output = append(output, "") output = append(output, "")
} }

View file

@ -403,7 +403,7 @@ func (fh *FunctionHandler) HandleFuncCall(line preproc.Line) ([]string, error) {
// Generate final assembly // Generate final assembly
asmLines = append(asmLines, inAssigns...) asmLines = append(asmLines, inAssigns...)
asmLines = append(asmLines, fmt.Sprintf(" jsr %s", funcName)) asmLines = append(asmLines, fmt.Sprintf("\tjsr %s", funcName))
asmLines = append(asmLines, outAssigns...) asmLines = append(asmLines, outAssigns...)
return asmLines, nil return asmLines, nil
@ -451,10 +451,10 @@ func (fh *FunctionHandler) processLabelArg(arg string, param *FuncParam, funcNam
} }
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #<%s", labelName), fmt.Sprintf("\tlda #<%s", labelName),
fmt.Sprintf(" sta %s", param.Symbol.FullName()), fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
fmt.Sprintf(" lda #>%s", labelName), fmt.Sprintf("\tlda #>%s", labelName),
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()), fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
) )
return nil return nil
@ -475,10 +475,10 @@ func (fh *FunctionHandler) processStringArg(arg string, param *FuncParam, funcNa
actualLabel := fh.constStrHandler.AddConstStr(labelName, arg, true, pragmaSet) actualLabel := fh.constStrHandler.AddConstStr(labelName, arg, true, pragmaSet)
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #<%s", actualLabel), fmt.Sprintf("\tlda #<%s", actualLabel),
fmt.Sprintf(" sta %s", param.Symbol.FullName()), fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
fmt.Sprintf(" lda #>%s", actualLabel), fmt.Sprintf("\tlda #>%s", actualLabel),
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()), fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
) )
return nil return nil
@ -492,20 +492,20 @@ func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName
// Generate IN assignments (sym -> param) // Generate IN assignments (sym -> param)
if param.Direction.Has(DirIn) { if param.Direction.Has(DirIn) {
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf(" lda %s", sym.FullName()), fmt.Sprintf("\tlda %s", sym.FullName()),
fmt.Sprintf(" sta %s", param.Symbol.FullName()), fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
) )
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
if sym.IsWord() { if sym.IsWord() {
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf(" lda %s+1", sym.FullName()), fmt.Sprintf("\tlda %s+1", sym.FullName()),
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()), fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
) )
} else { } else {
// byte -> word: zero extend // byte -> word: zero extend
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
" lda #0", "\tlda #0",
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()), fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
) )
} }
} else if sym.IsWord() { } else if sym.IsWord() {
@ -518,20 +518,20 @@ func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName
// Generate OUT assignments (param -> sym) // Generate OUT assignments (param -> sym)
if param.Direction.Has(DirOut) { if param.Direction.Has(DirOut) {
*outAssigns = append(*outAssigns, *outAssigns = append(*outAssigns,
fmt.Sprintf(" lda %s", param.Symbol.FullName()), fmt.Sprintf("\tlda %s", param.Symbol.FullName()),
fmt.Sprintf(" sta %s", sym.FullName()), fmt.Sprintf("\tsta %s", sym.FullName()),
) )
if sym.IsWord() { if sym.IsWord() {
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
*outAssigns = append(*outAssigns, *outAssigns = append(*outAssigns,
fmt.Sprintf(" lda %s+1", param.Symbol.FullName()), fmt.Sprintf("\tlda %s+1", param.Symbol.FullName()),
fmt.Sprintf(" sta %s+1", sym.FullName()), fmt.Sprintf("\tsta %s+1", sym.FullName()),
) )
} else { } else {
// byte -> word: zero extend // byte -> word: zero extend
*outAssigns = append(*outAssigns, *outAssigns = append(*outAssigns,
" lda #0", "\tlda #0",
fmt.Sprintf(" sta %s+1", sym.FullName()), fmt.Sprintf("\tsta %s+1", sym.FullName()),
) )
} }
} else if param.Symbol.IsWord() { } else if param.Symbol.IsWord() {
@ -576,16 +576,16 @@ func (fh *FunctionHandler) processConstArg(arg string, param *FuncParam, funcNam
highByte := uint8((value >> 8) & 0xFF) highByte := uint8((value >> 8) & 0xFF)
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #%d", lowByte), fmt.Sprintf("\tlda #%d", lowByte),
fmt.Sprintf(" sta %s", param.Symbol.FullName()), fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
) )
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
// Optimize: only reload A if high byte differs // Optimize: only reload A if high byte differs
if highByte != lowByte { if highByte != lowByte {
*inAssigns = append(*inAssigns, fmt.Sprintf(" lda #%d", highByte)) *inAssigns = append(*inAssigns, fmt.Sprintf("\tlda #%d", highByte))
} }
*inAssigns = append(*inAssigns, fmt.Sprintf(" sta %s+1", param.Symbol.FullName())) *inAssigns = append(*inAssigns, fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()))
} }
return nil return nil
@ -604,16 +604,16 @@ func (fh *FunctionHandler) processConstValue(value uint16, param *FuncParam, fun
highByte := uint8((value >> 8) & 0xFF) highByte := uint8((value >> 8) & 0xFF)
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #%d", lowByte), fmt.Sprintf("\tlda #%d", lowByte),
fmt.Sprintf(" sta %s", param.Symbol.FullName()), fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
) )
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
// Optimize: only reload A if high byte differs // Optimize: only reload A if high byte differs
if highByte != lowByte { if highByte != lowByte {
*inAssigns = append(*inAssigns, fmt.Sprintf(" lda #%d", highByte)) *inAssigns = append(*inAssigns, fmt.Sprintf("\tlda #%d", highByte))
} }
*inAssigns = append(*inAssigns, fmt.Sprintf(" sta %s+1", param.Symbol.FullName())) *inAssigns = append(*inAssigns, fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()))
} }
return nil return nil
@ -917,10 +917,10 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
// AbsoluteOverlap represents a detected overlap in absolute addresses // AbsoluteOverlap represents a detected overlap in absolute addresses
type AbsoluteOverlap struct { type AbsoluteOverlap struct {
Func1 string // First function using the address Func1 string // First function using the address
Func2 string // Second function using the address Func2 string // Second function using the address
Address uint16 // Overlapping address Address uint16 // Overlapping address
CallChain []string // Call chain from Func1 to Func2 CallChain []string // Call chain from Func1 to Func2
} }
// AnalyzeAbsoluteOverlaps checks for overlapping absolute addresses in call chains // AnalyzeAbsoluteOverlaps checks for overlapping absolute addresses in call chains

View file

@ -399,13 +399,13 @@ func TestHandleFuncCall_VarArgs(t *testing.T) {
// Check generated assembly // Check generated assembly
expectedLines := []string{ expectedLines := []string{
" lda var_a", "\tlda var_a",
" sta test_func_param_a", "\tsta test_func_param_a",
" lda var_b", "\tlda var_b",
" sta test_func_param_b", "\tsta test_func_param_b",
" lda var_b+1", "\tlda var_b+1",
" sta test_func_param_b+1", "\tsta test_func_param_b+1",
" jsr test_func", "\tjsr test_func",
} }
if len(asm) != len(expectedLines) { if len(asm) != len(expectedLines) {
@ -928,13 +928,13 @@ func TestHandleFuncCall_AbsoluteParams(t *testing.T) {
// Check generated assembly uses correct names // Check generated assembly uses correct names
expectedLines := []string{ expectedLines := []string{
" lda var_a", "\tlda var_a",
" sta test_abs_param_a", "\tsta test_abs_param_a",
" lda var_b", "\tlda var_b",
" sta test_abs_param_b", "\tsta test_abs_param_b",
" lda var_b+1", "\tlda var_b+1",
" sta test_abs_param_b+1", "\tsta test_abs_param_b+1",
" jsr test_abs", "\tjsr test_abs",
} }
if len(asm) != len(expectedLines) { if len(asm) != len(expectedLines) {

View file

@ -7,11 +7,11 @@ import "strings"
// @@OPT markers are stripped from output. // @@OPT markers are stripped from output.
// Returns optimized lines and the set of dissolved REGISTER variable names // Returns optimized lines and the set of dissolved REGISTER variable names
// (variables that had all their stores/loads eliminated). // (variables that had all their stores/loads eliminated).
func Optimize(lines []string, cfg *Config) ([]string, map[string]bool) { func Optimize(lines []SourceLine, cfg *Config) ([]string, map[string]bool) {
dissolved := map[string]bool{} dissolved := map[string]bool{}
if cfg == nil || !cfg.Any() { if cfg == nil || !cfg.Any() {
return lines, dissolved return SourceLineTexts(lines), dissolved
} }
parsed := parseLines(lines) parsed := parseLines(lines)
@ -19,6 +19,7 @@ func Optimize(lines []string, cfg *Config) ([]string, map[string]bool) {
if cfg.EnableStoreLoad { if cfg.EnableStoreLoad {
parsed = passStoreReload(parsed, cfg) parsed = passStoreReload(parsed, cfg)
parsed = passStoreTransfer(parsed, cfg)
} }
if cfg.EnableLoad { if cfg.EnableLoad {
parsed = passLoadElimination(parsed, cfg) parsed = passLoadElimination(parsed, cfg)

View file

@ -5,12 +5,26 @@ import (
"testing" "testing"
) )
func lines(s ...string) []string { return s } func lines(s ...string) []SourceLine {
out := make([]SourceLine, len(s))
for i, t := range s {
out[i] = SourceLine{Text: t, Origin: OriginGenerated}
}
return out
}
func verbatimLines(s ...string) []SourceLine {
out := make([]SourceLine, len(s))
for i, t := range s {
out[i] = SourceLine{Text: t, Origin: OriginAsm}
}
return out
}
func TestPassLoadElimination(t *testing.T) { func TestPassLoadElimination(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []string input []SourceLine
expected int // expected number of lines after optimization expected int // expected number of lines after optimization
}{ }{
{ {
@ -60,7 +74,7 @@ func TestPassLoadElimination(t *testing.T) {
func TestPassImmElimination(t *testing.T) { func TestPassImmElimination(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []string input []SourceLine
expected int expected int
}{ }{
{ {
@ -102,7 +116,7 @@ func TestPassImmElimination(t *testing.T) {
func TestPassJmpNext(t *testing.T) { func TestPassJmpNext(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []string input []SourceLine
expected int expected int
}{ }{
{ {
@ -137,7 +151,7 @@ func TestPassJmpNext(t *testing.T) {
func TestPassSelfAssignment(t *testing.T) { func TestPassSelfAssignment(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []string input []SourceLine
expected int expected int
}{ }{
{ {
@ -196,6 +210,15 @@ func TestPassLoadIO(t *testing.T) {
} }
}) })
t.Run("decimal IO load not eliminated", func(t *testing.T) {
parsed := parseLines(lines("\tlda 53266", "\tlda 53266"))
result := passLoadElimination(parsed, cfg)
cleaned := stripOptMarkers(result)
if len(cleaned) != 2 {
t.Errorf("expected 2 lines (decimal IO skip), got %d", len(cleaned))
}
})
t.Run("variable name not caught by IO", func(t *testing.T) { t.Run("variable name not caught by IO", func(t *testing.T) {
parsed := parseLines(lines("\tlda RASTER_LINE", "\tlda RASTER_LINE")) parsed := parseLines(lines("\tlda RASTER_LINE", "\tlda RASTER_LINE"))
result := passLoadElimination(parsed, cfg) result := passLoadElimination(parsed, cfg)
@ -241,6 +264,25 @@ func TestOptimizeIntegration(t *testing.T) {
} }
} }
func TestOptimizeStoreTransferIntegration(t *testing.T) {
input := lines(
"\tlda #$05",
"\tsta x",
"\tldy x",
"\tlda (zp),y",
)
cfg := &Config{EnableStoreLoad: true}
output, _ := Optimize(input, cfg)
if len(output) != 4 {
t.Fatalf("expected 4 lines, got %d:\n%v", len(output), output)
}
if output[2] != "\ttay" {
t.Errorf("expected ldy x to become tay via Optimize, got %q", output[2])
}
}
func TestOptimizeWithDebug(t *testing.T) { func TestOptimizeWithDebug(t *testing.T) {
input := lines( input := lines(
"\tlda x", "\tlda x",
@ -289,7 +331,7 @@ func TestPassStoreReload(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []string input []SourceLine
expected int expected int
}{ }{
{ {
@ -447,13 +489,12 @@ func TestPassStoreReloadIO(t *testing.T) {
t.Run("decimal address in IO range", func(t *testing.T) { t.Run("decimal address in IO range", func(t *testing.T) {
cfg := &Config{} cfg := &Config{}
cfg.IOMap[0xD020] = true cfg.IOMap[0xD020] = true
// Decimal 53280 = $D020, but IOMap uses hex lookup // Decimal 53280 = $D020, now caught by IOMap regardless of base
// The pass checks $ prefix only, decimal addresses won't be caught by IOMap
parsed := parseLines(lines("\tsta 53280", "\tlda 53280")) parsed := parseLines(lines("\tsta 53280", "\tlda 53280"))
result := passStoreReload(parsed, cfg) result := passStoreReload(parsed, cfg)
cleaned := stripOptMarkers(result) cleaned := stripOptMarkers(result)
if len(cleaned) != 1 { if len(cleaned) != 2 {
t.Errorf("expected 1 line (decimal not caught by I/O), got %d", len(cleaned)) t.Errorf("expected 2 lines (decimal I/O protected), got %d", len(cleaned))
} }
}) })
@ -497,8 +538,8 @@ func TestPassStoreReloadIO(t *testing.T) {
{Start: 0xD000, End: 0xDFFF}, {Start: 0xD000, End: 0xDFFF},
{Start: 0xDC00, End: 0xDC0F}, {Start: 0xDC00, End: 0xDC0F},
}) })
tests := []struct{ tests := []struct {
addr string addr string
expect int expect int
}{ }{
{"$D020", 2}, {"$D020", 2},
@ -508,7 +549,7 @@ func TestPassStoreReloadIO(t *testing.T) {
{"$E000", 1}, {"$E000", 1},
} }
for _, tt := range tests { for _, tt := range tests {
parsed := parseLines(lines("\tsta " + tt.addr, "\tlda " + tt.addr)) parsed := parseLines(lines("\tsta "+tt.addr, "\tlda "+tt.addr))
result := passStoreReload(parsed, cfg) result := passStoreReload(parsed, cfg)
cleaned := stripOptMarkers(result) cleaned := stripOptMarkers(result)
if len(cleaned) != tt.expect { if len(cleaned) != tt.expect {
@ -518,12 +559,117 @@ func TestPassStoreReloadIO(t *testing.T) {
}) })
} }
func TestPassStoreTransfer(t *testing.T) {
cfg := &Config{}
t.Run("sta x then ldy x becomes tay", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[0] != "\tsta x" || out[1] != "\ttay" {
t.Errorf("got %v", out)
}
})
t.Run("sta x then ldx x becomes tax", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldx x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[0] != "\tsta x" || out[1] != "\ttax" {
t.Errorf("got %v", out)
}
})
t.Run("comment between is skipped", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "; source", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 3 || out[2] != "\ttay" {
t.Errorf("got %v", out)
}
})
t.Run("label between blocks transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "label", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 3 || out[2] != "\tldy x" {
t.Errorf("got %v", out)
}
})
t.Run("IO address not transferred", func(t *testing.T) {
ioCfg := &Config{}
ioCfg.IOMap[0xD020] = true
parsed := parseLines(lines("\tsta $D020", "\tldy $D020"))
result := passStoreTransfer(parsed, ioCfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy $D020" {
t.Errorf("got %v", out)
}
})
t.Run("operand mismatch no transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldy y"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy y" {
t.Errorf("got %v", out)
}
})
t.Run("inline comment on load blocks transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldy x ; note"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy x ; note" {
t.Errorf("got %v", out)
}
})
t.Run("indexed store not transferred", func(t *testing.T) {
parsed := parseLines(lines("\tsta (zp),y", "\tldy (zp),y"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy (zp),y" {
t.Errorf("got %v", out)
}
})
t.Run("lda between blocks transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tlda y", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 3 || out[2] != "\tldy x" {
t.Errorf("got %v", out)
}
})
t.Run("sta x then lda x not transferred", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tlda x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tlda x" {
t.Errorf("got %v", out)
}
})
t.Run("sta as last line no transfer", func(t *testing.T) {
parsed := parseLines(lines("\tlda #1", "\tsta x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tsta x" {
t.Errorf("got %v", out)
}
})
}
func TestPassRegDead_DeadStore(t *testing.T) { func TestPassRegDead_DeadStore(t *testing.T) {
input := []string{ input := lines(
"\tlda #$05", "\tlda #$05",
"\tsta myFunc_temp", "\tsta myFunc_temp",
"\tsta $d020", "\tsta $d020",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -545,12 +691,12 @@ func TestPassRegDead_DeadStore(t *testing.T) {
} }
func TestPassRegDead_KeptStoreWithLoad(t *testing.T) { func TestPassRegDead_KeptStoreWithLoad(t *testing.T) {
input := []string{ input := lines(
"\tlda #$05", "\tlda #$05",
"\tsta myFunc_temp", "\tsta myFunc_temp",
"\tlda myFunc_temp", "\tlda myFunc_temp",
"\tsta $d020", "\tsta $d020",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -573,12 +719,12 @@ func TestPassRegDead_KeptStoreWithLoad(t *testing.T) {
func TestPassRegDead_KeptStoreAtLabel(t *testing.T) { func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
// sta regVar before a label — no lda regVar anywhere → globally dead // sta regVar before a label — no lda regVar anywhere → globally dead
input := []string{ input := lines(
"\tlda #$05", "\tlda #$05",
"\tsta myFunc_temp", "\tsta myFunc_temp",
"myskip:", "myskip:",
"\tnop", "\tnop",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -600,15 +746,15 @@ func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
} }
func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) { func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) {
input := []string{ input := lines(
"\tlda #$05", "\tlda #$05",
"\tsta normalVar", "\tsta normalVar",
"\tsta $d020", "\tsta $d020",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
RegisterVars: map[string]bool{}, RegisterVars: map[string]bool{},
} }
output, _ := Optimize(input, cfg) output, _ := Optimize(input, cfg)
@ -620,12 +766,12 @@ func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) {
} }
func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) { func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) {
input := []string{ input := lines(
"\tlda #$05", "\tlda #$05",
"\tsta myFunc_temp", "\tsta myFunc_temp",
"\tsta $d020", "\tsta $d020",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -648,7 +794,7 @@ func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) {
func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) { func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
// sta regVar; lda other → A clobbered, but regVar is reloaded later → keep store // sta regVar; lda other → A clobbered, but regVar is reloaded later → keep store
input := []string{ input := lines(
"\tlda 53281", "\tlda 53281",
"\tsta spill_me_bg", "\tsta spill_me_bg",
"\tlda 53282", "\tlda 53282",
@ -656,7 +802,7 @@ func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
"\tlda spill_me_bg", "\tlda spill_me_bg",
"\tsta 53282", "\tsta 53282",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -680,14 +826,14 @@ func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) { func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) {
// sta → and (A clobbered) → POKE (uses A directly) → rts. Store dead. // sta → and (A clobbered) → POKE (uses A directly) → rts. Store dead.
input := []string{ input := lines(
"\tlda 56576", "\tlda 56576",
"\tsta dissolve_me_temp", "\tsta dissolve_me_temp",
"\tand #$fc", "\tand #$fc",
"\tsta dissolve_me_temp", "\tsta dissolve_me_temp",
"\tsta 56576", "\tsta 56576",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -711,12 +857,12 @@ func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) {
func TestPassRegDead_GloballyDeadBeforeJsr(t *testing.T) { func TestPassRegDead_GloballyDeadBeforeJsr(t *testing.T) {
// sta regVar; jsr foo; no lda regVar anywhere → globally dead // sta regVar; jsr foo; no lda regVar anywhere → globally dead
// jsr does not protect a store with zero readers. // jsr does not protect a store with zero readers.
input := []string{ input := lines(
"\tlda 53281", "\tlda 53281",
"\tsta call_val", "\tsta call_val",
"\tjsr helper", "\tjsr helper",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -741,14 +887,14 @@ func TestPassRegDead_JsrProtectsStoreWhenReloaded(t *testing.T) {
// sta regVar; jsr foo; lda regVar → store kept // sta regVar; jsr foo; lda regVar → store kept
// lda exists (pre-scan passes), but jsr blocks local scan before reaching it. // lda exists (pre-scan passes), but jsr blocks local scan before reaching it.
// The callee may modify A, so the reload after jsr is genuine. // The callee may modify A, so the reload after jsr is genuine.
input := []string{ input := lines(
"\tlda 53281", "\tlda 53281",
"\tsta call_val", "\tsta call_val",
"\tjsr helper", "\tjsr helper",
"\tlda call_val", "\tlda call_val",
"\tsta 53280", "\tsta 53280",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -771,13 +917,13 @@ func TestPassRegDead_JsrProtectsStoreWhenReloaded(t *testing.T) {
func TestPassRegDead_InitValueFlowsThrough(t *testing.T) { func TestPassRegDead_InitValueFlowsThrough(t *testing.T) {
// BYTE REGISTER x = 42; POKE $d020, x → value flows through A, never touches RAM // BYTE REGISTER x = 42; POKE $d020, x → value flows through A, never touches RAM
input := []string{ input := lines(
"\tlda #$2a", "\tlda #$2a",
"\tsta test_x", "\tsta test_x",
"\tlda test_x", "\tlda test_x",
"\tsta 53280", "\tsta 53280",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -805,7 +951,7 @@ func TestPassRegDead_InitValueFlowsThrough(t *testing.T) {
func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) { func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
// sta regVar before a label, no lda regVar anywhere → globally dead // sta regVar before a label, no lda regVar anywhere → globally dead
// Typical copy-loop pattern: the value flows through A, never reloaded. // Typical copy-loop pattern: the value flows through A, never reloaded.
input := []string{ input := lines(
"_LOOPSTART", "_LOOPSTART",
"\tlda (src),y", "\tlda (src),y",
"\tsta loop_val", "\tsta loop_val",
@ -814,7 +960,7 @@ func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
"_SKIP:", "_SKIP:",
"\tjmp _LOOPSTART", "\tjmp _LOOPSTART",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -838,7 +984,7 @@ func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) { func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
// sta regVar before a label, but lda regVar exists elsewhere → not globally dead // sta regVar before a label, but lda regVar exists elsewhere → not globally dead
// Falls through to local scan. With jsr before the lda, store is kept. // Falls through to local scan. With jsr before the lda, store is kept.
input := []string{ input := lines(
"\tsta spill_me_bg", "\tsta spill_me_bg",
"\tlda 53282", "\tlda 53282",
"_SKIP:", "_SKIP:",
@ -846,7 +992,7 @@ func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
"\tlda spill_me_bg", "\tlda spill_me_bg",
"\tsta 53282", "\tsta 53282",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -869,181 +1015,181 @@ func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
func TestPassRegDead_ReadModifyWrite(t *testing.T) { func TestPassRegDead_ReadModifyWrite(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
opcode string opcode string
lines []string lines []SourceLine
}{ }{
{ {
name: "dec reads stored value", name: "dec reads stored value",
opcode: "dec", opcode: "dec",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tdec rmw_var", "\tdec rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "inc reads stored value", name: "inc reads stored value",
opcode: "inc", opcode: "inc",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tinc rmw_var", "\tinc rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "adc reads stored value", name: "adc reads stored value",
opcode: "adc", opcode: "adc",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tclc", "\tclc",
"\tadc rmw_var", "\tadc rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "sbc reads stored value", name: "sbc reads stored value",
opcode: "sbc", opcode: "sbc",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tsec", "\tsec",
"\tsbc rmw_var", "\tsbc rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "and reads stored value", name: "and reads stored value",
opcode: "and", opcode: "and",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tand rmw_var", "\tand rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "ora reads stored value", name: "ora reads stored value",
opcode: "ora", opcode: "ora",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tora rmw_var", "\tora rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "eor reads stored value", name: "eor reads stored value",
opcode: "eor", opcode: "eor",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\teor rmw_var", "\teor rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "cmp reads stored value", name: "cmp reads stored value",
opcode: "cmp", opcode: "cmp",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tcmp rmw_var", "\tcmp rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "ldx reads stored value", name: "ldx reads stored value",
opcode: "ldx", opcode: "ldx",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tldx rmw_var", "\tldx rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "ldy reads stored value", name: "ldy reads stored value",
opcode: "ldy", opcode: "ldy",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tldy rmw_var", "\tldy rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "asl reads stored value", name: "asl reads stored value",
opcode: "asl", opcode: "asl",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tasl rmw_var", "\tasl rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "lsr reads stored value", name: "lsr reads stored value",
opcode: "lsr", opcode: "lsr",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tlsr rmw_var", "\tlsr rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "rol reads stored value", name: "rol reads stored value",
opcode: "rol", opcode: "rol",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\trol rmw_var", "\trol rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "ror reads stored value", name: "ror reads stored value",
opcode: "ror", opcode: "ror",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tror rmw_var", "\tror rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "bit reads stored value", name: "bit reads stored value",
opcode: "bit", opcode: "bit",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tbit rmw_var", "\tbit rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "cpx reads stored value", name: "cpx reads stored value",
opcode: "cpx", opcode: "cpx",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tcpx rmw_var", "\tcpx rmw_var",
"\trts", "\trts",
}, ),
}, },
{ {
name: "cpy reads stored value", name: "cpy reads stored value",
opcode: "cpy", opcode: "cpy",
lines: []string{ lines: lines(
"\tlda #$05", "\tlda #$05",
"\tsta rmw_var", "\tsta rmw_var",
"\tcpy rmw_var", "\tcpy rmw_var",
"\trts", "\trts",
}, ),
}, },
} }
@ -1074,7 +1220,7 @@ func TestPassRegDead_JmpDoesNotKillStore(t *testing.T) {
// sta regVar; ...(then body)...; jmp _END; _ELSE:; ...; _END:; ldy regVar // sta regVar; ...(then body)...; jmp _END; _ELSE:; ...; _END:; ldy regVar
// The jmp in the THEN body should NOT kill the store because // The jmp in the THEN body should NOT kill the store because
// the jump target _END reaches code that reads regVar. // the jump target _END reaches code that reads regVar.
input := []string{ input := lines(
"\tldy #0", "\tldy #0",
"\tlda (zp),y", "\tlda (zp),y",
"\tsta if_val", "\tsta if_val",
@ -1092,7 +1238,7 @@ func TestPassRegDead_JmpDoesNotKillStore(t *testing.T) {
"\tldy if_val", "\tldy if_val",
"\tlda (zp),y", "\tlda (zp),y",
"\trts", "\trts",
} )
cfg := &Config{ cfg := &Config{
EnableRegisterVars: true, EnableRegisterVars: true,
@ -1112,3 +1258,103 @@ func TestPassRegDead_JmpDoesNotKillStore(t *testing.T) {
t.Errorf("expected sta if_val to be kept (jmp in THEN should not kill it), got:\n%s", joined) t.Errorf("expected sta if_val to be kept (jmp in THEN should not kill it), got:\n%s", joined)
} }
} }
func TestParseLinesOrigin(t *testing.T) {
t.Run("generated tab line is code", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "\tlda x", Origin: OriginGenerated}})
if len(parsed) != 1 || !parsed[0].isCode {
t.Errorf("generated tab line should be code, got %+v", parsed)
}
})
t.Run("generated space line is code", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: " lda x", Origin: OriginGenerated}})
if len(parsed) != 1 || !parsed[0].isCode {
t.Errorf("generated space-indented line should be code, got %+v", parsed)
}
})
t.Run("verbatim ASM tab line is a barrier, not code", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "\tlda x", Origin: OriginAsm}})
if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel {
t.Errorf("verbatim ASM line should be a barrier, got %+v", parsed)
}
})
t.Run("verbatim SCRIPT tab line is a barrier", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "\tsta $d020", Origin: OriginScript}})
if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel {
t.Errorf("verbatim SCRIPT line should be a barrier, got %+v", parsed)
}
})
t.Run("verbatim ASM comment stays a comment", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "; note", Origin: OriginAsm}})
if len(parsed) != 1 || !parsed[0].isComment {
t.Errorf("verbatim ASM comment should be a comment, got %+v", parsed)
}
})
t.Run("verbatim ASM label stays a barrier", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "h_dispatch:", Origin: OriginAsm}})
if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel {
t.Errorf("verbatim ASM label should be a barrier, got %+v", parsed)
}
})
}
func TestOptimizeSkipsVerbatimTransfer(t *testing.T) {
// A tab-indented ASM block line must NOT be rewritten, even though it
// looks like generated code: provenance trumps indentation.
input := []SourceLine{
{Text: "\tsta x", Origin: OriginAsm},
{Text: "\tldy x", Origin: OriginAsm},
}
cfg := &Config{EnableStoreLoad: true}
output, _ := Optimize(input, cfg)
if len(output) != 2 || output[0] != "\tsta x" || output[1] != "\tldy x" {
t.Errorf("verbatim ASM lines must not be optimized, got %v", output)
}
}
func TestPassRegDead_OriginAffectsReadDetection(t *testing.T) {
regVars := map[string]bool{"bg": true}
t.Run("generated read keeps store", func(t *testing.T) {
input := []SourceLine{
{Text: "\tlda 53281", Origin: OriginGenerated},
{Text: "\tsta bg", Origin: OriginGenerated},
{Text: "\tlda bg", Origin: OriginGenerated},
{Text: "\tsta 53280", Origin: OriginGenerated},
{Text: "\trts", Origin: OriginGenerated},
}
out, dissolved := Optimize(input, &Config{EnableRegisterVars: true, RegisterVars: regVars})
if dissolved["bg"] {
t.Error("bg should NOT dissolve: a generated read exists")
}
if !strings.Contains(strings.Join(out, "\n"), "sta bg") {
t.Errorf("expected sta bg kept (generated read), got:\n%s", strings.Join(out, "\n"))
}
})
t.Run("verbatim ASM read does not keep store", func(t *testing.T) {
input := []SourceLine{
{Text: "\tlda 53281", Origin: OriginGenerated},
{Text: "\tsta bg", Origin: OriginGenerated},
{Text: "\tlda bg", Origin: OriginAsm},
{Text: "\tsta 53280", Origin: OriginGenerated},
{Text: "\trts", Origin: OriginGenerated},
}
out, _ := Optimize(input, &Config{EnableRegisterVars: true, RegisterVars: regVars})
joined := strings.Join(out, "\n")
if strings.Contains(joined, "sta bg") {
t.Errorf("expected sta bg removed (verbatim ASM read is not a generated read), got:\n%s", joined)
}
// The verbatim ASM line itself must be preserved untouched.
if !strings.Contains(joined, "\tlda bg") {
t.Errorf("expected verbatim lda bg preserved, got:\n%s", joined)
}
})
}

View file

@ -76,14 +76,12 @@ 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. // isIOAddr returns true if operand is a numeric address (hex or decimal) marked
// as I/O in the config. Symbolic names parse to -1 and are never treated as I/O.
func isIOAddr(operand string, cfg *Config) bool { func isIOAddr(operand string, cfg *Config) bool {
if cfg == nil { if cfg == nil {
return false return false
} }
if !strings.HasPrefix(operand, "$") {
return false
}
addr := parseHexOrDec(operand) addr := parseHexOrDec(operand)
return addr >= 0 && addr < 65536 && cfg.IOMap[addr] return addr >= 0 && addr < 65536 && cfg.IOMap[addr]
} }

View file

@ -79,8 +79,9 @@ func isSafeStldOperand(operand string, cfg *Config) bool {
return false return false
} }
// Check if operand is a direct hex address in an I/O region // Check if operand is a direct numeric address (hex or decimal) in an I/O region.
if strings.HasPrefix(operand, "$") { // Symbolic names parse to -1 and are never treated as I/O.
if cfg != nil {
addr := parseHexOrDec(operand) addr := parseHexOrDec(operand)
if addr >= 0 && addr < 65536 && cfg.IOMap[addr] { if addr >= 0 && addr < 65536 && cfg.IOMap[addr] {
return false return false

View file

@ -0,0 +1,67 @@
package optimizer
import "strings"
// passStoreTransfer converts a store immediately followed by a reload of the
// same value into a different register, into a register-transfer instruction.
//
// sta M; ldy M → sta M; tay
// sta M; ldx M → sta M; tax
//
// A already holds M after the store, so the reload from memory is redundant.
// Only comments and @@OPT markers may separate the two instructions; a label
// or any other code line blocks the transform.
func passStoreTransfer(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) {
if j, transfer := findTransferTarget(lines, i+1, line.operand); transfer != "" {
result = append(result, line)
for k := i + 1; k < j; k++ {
result = append(result, lines[k])
}
result = append(result, asmLine{
text: "\t" + transfer,
isCode: true,
opcode: transfer,
})
i = j
continue
}
}
result = append(result, line)
}
return result
}
// findTransferTarget scans forward from start, skipping comments and @@OPT
// markers, for an ldy/ldx of the given operand. Returns the index of that line
// and the transfer opcode to use, or "" if no safe match is found.
func findTransferTarget(lines []asmLine, start int, operand string) (int, string) {
i := start
for i < len(lines) && (lines[i].isComment || lines[i].optMarker) {
i++
}
if i >= len(lines) || !lines[i].isCode {
return 0, ""
}
next := lines[i]
if next.operand != operand || len(strings.Fields(next.text)) != 2 {
return 0, ""
}
switch next.opcode {
case "ldy":
return i, "tay"
case "ldx":
return i, "tax"
default:
return 0, ""
}
}

View file

@ -41,9 +41,36 @@ type asmLine struct {
operand string operand string
} }
func parseLines(lines []string) []asmLine { // Origin identifies the source of an output line. The optimizer must know
// whether a line was produced by the compiler itself (and is therefore safe
// to optimize) or emitted verbatim from an ASM block or SCRIPT output.
type Origin int
const (
OriginGenerated Origin = iota // compiler-generated code (optimizable)
OriginAsm // handwritten ASM block content (verbatim)
OriginScript // SCRIPT print() output (verbatim)
)
// SourceLine is a single output line together with its provenance.
type SourceLine struct {
Text string
Origin Origin
}
// SourceLineTexts extracts just the text from a slice of source lines.
func SourceLineTexts(lines []SourceLine) []string {
out := make([]string, len(lines))
for i, l := range lines {
out[i] = l.Text
}
return out
}
func parseLines(lines []SourceLine) []asmLine {
var result []asmLine var result []asmLine
for _, l := range lines { for _, sl := range lines {
l := sl.Text
al := asmLine{text: l} al := asmLine{text: l}
if l == "" { if l == "" {
@ -64,14 +91,21 @@ func parseLines(lines []string) []asmLine {
continue continue
} }
if l[0] == '\t' { // Only compiler-generated, indented lines are treated as optimizable
al.isCode = true // instructions. ACME requires labels at column 0, so any leading
// whitespace means "not a label" — the exact character (tab or space)
// is irrelevant. Generated labels and any verbatim ASM/SCRIPT line
// become barriers.
if sl.Origin == OriginGenerated && isIndented(l) {
parts := strings.Fields(l) parts := strings.Fields(l)
if len(parts) > 0 { if len(parts) > 0 {
al.isCode = true
al.opcode = strings.ToLower(parts[0]) al.opcode = strings.ToLower(parts[0])
} if len(parts) > 1 {
if len(parts) > 1 { al.operand = parts[1]
al.operand = parts[1] }
} else {
al.isLabel = true
} }
} else { } else {
al.isLabel = true al.isLabel = true
@ -82,6 +116,12 @@ func parseLines(lines []string) []asmLine {
return result return result
} }
// isIndented reports whether a line has leading whitespace (an instruction),
// as opposed to a label which starts at column 0.
func isIndented(l string) bool {
return len(l) > 0 && (l[0] == ' ' || l[0] == '\t')
}
// stripOptMarkers removes @@OPT comment lines from the output // stripOptMarkers removes @@OPT comment lines from the output
func stripOptMarkers(lines []asmLine) []asmLine { func stripOptMarkers(lines []asmLine) []asmLine {
var result []asmLine var result []asmLine
@ -106,4 +146,3 @@ func linesToString(lines []asmLine) []string {
func skipJmpMarker(line asmLine) bool { func skipJmpMarker(line asmLine) bool {
return line.optMarker return line.optMarker
} }