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:
parent
7d5d0ac7f3
commit
97c976c218
10 changed files with 576 additions and 179 deletions
|
|
@ -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
|
||||
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
|
||||
- **`C65LIBPATH`**: Search path for `#INCLUDE <file>` directives
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import (
|
|||
type Compiler struct {
|
||||
ctx *CompilerContext
|
||||
registry *CommandRegistry
|
||||
deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma
|
||||
dissolvedVars map[string]bool // REGISTER vars dissolved by optimizer
|
||||
CmdlineOpt bool // --opt enables all passes
|
||||
CmdlineDebug bool // --opt-debug enables debug output
|
||||
CmdlineIORegions []optimizer.IORegion // --opt-exclude ranges
|
||||
deferredAsm []optimizer.SourceLine // ASM blocks with _P_ASM_AFTER_VARS pragma
|
||||
dissolvedVars map[string]bool // REGISTER vars dissolved by optimizer
|
||||
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
|
||||
|
|
@ -39,9 +39,51 @@ func (c *Compiler) Registry() *CommandRegistry {
|
|||
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
|
||||
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||
var codeOutput []string
|
||||
var codeOutput []optimizer.SourceLine
|
||||
var lastKind = preproc.Source
|
||||
var scriptBuffer []preproc.Line
|
||||
var scriptIsLibrary bool
|
||||
|
|
@ -50,7 +92,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
var currentMacroParams []string
|
||||
var currentMacroSourceFile string
|
||||
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
|
||||
c.deferredAsm = nil
|
||||
|
|
@ -64,12 +106,12 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("script execution failed: %w", err)
|
||||
}
|
||||
codeOutput = append(codeOutput, scriptOutput...)
|
||||
codeOutput = append(codeOutput, scriptSourceLines(scriptOutput)...)
|
||||
scriptBuffer = nil
|
||||
if scriptIsLibrary {
|
||||
codeOutput = append(codeOutput, "; ENDSCRIPT LIBRARY")
|
||||
codeOutput = append(codeOutput, generatedLine("; ENDSCRIPT LIBRARY"))
|
||||
} 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,
|
||||
StartLine: currentMacroStartLine,
|
||||
}
|
||||
codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
|
||||
codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName)))
|
||||
}
|
||||
macroBuffer = nil
|
||||
currentMacroName = ""
|
||||
|
|
@ -94,7 +136,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
|
||||
// Close previous Assembler block
|
||||
if lastKind == preproc.Assembler && currentAsmTarget != nil {
|
||||
*currentAsmTarget = append(*currentAsmTarget, "; ENDASM")
|
||||
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine("; ENDASM"))
|
||||
currentAsmTarget = nil
|
||||
}
|
||||
|
||||
|
|
@ -102,25 +144,25 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
if line.Kind == preproc.Assembler {
|
||||
// Check if ASM block should be deferred to end
|
||||
pragmaSet := c.ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
|
||||
asmAfterVars := pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "" &&
|
||||
pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "0"
|
||||
|
||||
asmAfterVars := pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "" &&
|
||||
pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "0"
|
||||
|
||||
if asmAfterVars {
|
||||
// Add inline comment and defer ASM block
|
||||
codeOutput = append(codeOutput, "; ASM block deferred to end of source")
|
||||
c.deferredAsm = append(c.deferredAsm,
|
||||
fmt.Sprintf("; ASM Block from %s, Line %d", line.Filename, line.LineNo))
|
||||
codeOutput = append(codeOutput, generatedLine("; ASM block deferred to end of source"))
|
||||
c.deferredAsm = append(c.deferredAsm,
|
||||
asmSourceLine(fmt.Sprintf("; ASM Block from %s, Line %d", line.Filename, line.LineNo)))
|
||||
currentAsmTarget = &c.deferredAsm
|
||||
} else {
|
||||
// Normal ASM block
|
||||
codeOutput = append(codeOutput, "; ASM")
|
||||
codeOutput = append(codeOutput, generatedLine("; ASM"))
|
||||
currentAsmTarget = &codeOutput
|
||||
}
|
||||
} else if line.Kind == preproc.Script {
|
||||
codeOutput = append(codeOutput, "; SCRIPT")
|
||||
codeOutput = append(codeOutput, generatedLine("; SCRIPT"))
|
||||
scriptIsLibrary = false
|
||||
} else if line.Kind == preproc.ScriptLibrary {
|
||||
codeOutput = append(codeOutput, "; SCRIPT LIBRARY")
|
||||
codeOutput = append(codeOutput, generatedLine("; SCRIPT LIBRARY"))
|
||||
scriptIsLibrary = true
|
||||
} else if line.Kind == preproc.ScriptMacroDef {
|
||||
// First line is the header - parse it
|
||||
|
|
@ -131,7 +173,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
}
|
||||
currentMacroName = name
|
||||
currentMacroParams = params
|
||||
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
|
||||
codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; %s", line.Text)))
|
||||
}
|
||||
|
||||
lastKind = line.Kind
|
||||
|
|
@ -145,7 +187,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
c.printErrorWithContext(lines, i, fmt.Errorf("internal error: ASM line without active ASM block"))
|
||||
return nil, fmt.Errorf("compilation failed")
|
||||
}
|
||||
|
||||
|
||||
text := line.Text
|
||||
|
||||
// Find comment boundary - only process |...| patterns in the code portion
|
||||
|
|
@ -177,9 +219,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
}
|
||||
|
||||
// Emit with comments showing invocation
|
||||
*currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; %s", text))
|
||||
*currentAsmTarget = append(*currentAsmTarget, macroOutput...)
|
||||
*currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; end @%s", macroName))
|
||||
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine(fmt.Sprintf("; %s", text)))
|
||||
*currentAsmTarget = append(*currentAsmTarget, asmSourceLines(macroOutput)...)
|
||||
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine(fmt.Sprintf("; end @%s", macroName)))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
@ -208,7 +250,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
// Continue searching after the replacement
|
||||
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 {
|
||||
// Collect script lines for execution
|
||||
scriptBuffer = append(scriptBuffer, line)
|
||||
|
|
@ -253,18 +295,18 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
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() {
|
||||
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
|
||||
if lastKind == preproc.Assembler {
|
||||
// Close the final ASM block if still open
|
||||
if currentAsmTarget != nil {
|
||||
*currentAsmTarget = append(*currentAsmTarget, "; ENDASM")
|
||||
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine("; ENDASM"))
|
||||
}
|
||||
return nil, fmt.Errorf("Unclosed ASM block.")
|
||||
} else if lastKind == preproc.Script {
|
||||
|
|
@ -276,10 +318,13 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
}
|
||||
|
||||
// Peephole optimization pass
|
||||
var codeStrings []string
|
||||
if cfg := c.getOptimizerConfig(); cfg != nil {
|
||||
var dissolved map[string]bool
|
||||
codeOutput, dissolved = optimizer.Optimize(codeOutput, cfg)
|
||||
codeStrings, dissolved = optimizer.Optimize(codeOutput, cfg)
|
||||
c.dissolvedVars = dissolved
|
||||
} else {
|
||||
codeStrings = optimizer.SourceLineTexts(codeOutput)
|
||||
}
|
||||
|
||||
// Analyze for overlapping absolute addresses in function call chains
|
||||
|
|
@ -287,7 +332,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
|
||||
// Get functions with _P_REMOVE_UNUSED pragma (for suppressing variable warnings)
|
||||
funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma()
|
||||
|
||||
|
||||
// Check for unused variables and print warnings (skip variables in functions with remove pragma)
|
||||
warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma, c.dissolvedVars)
|
||||
for _, warning := range warnings {
|
||||
|
|
@ -301,13 +346,13 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
}
|
||||
|
||||
// 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
|
||||
codeOutput = updatePeepholeHeader(codeOutput)
|
||||
codeStrings = updatePeepholeHeader(codeStrings)
|
||||
|
||||
// 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
|
||||
|
|
@ -757,7 +802,7 @@ func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bo
|
|||
if len(c.deferredAsm) > 0 {
|
||||
output = append(output, "; Deferred ASM blocks (after variables)")
|
||||
output = append(output, "")
|
||||
output = append(output, c.deferredAsm...)
|
||||
output = append(output, optimizer.SourceLineTexts(c.deferredAsm)...)
|
||||
output = append(output, "")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ type FunctionHandler struct {
|
|||
// Absolute address tracking for overlap detection
|
||||
absoluteAddrs map[string]map[uint16]bool // funcName -> set of absolute addresses used
|
||||
callGraph map[string][]string // funcName -> list of functions it calls
|
||||
|
||||
|
||||
// Function usage tracking for unused function warnings
|
||||
calledFunctions map[string]bool // funcName -> true if function is called
|
||||
|
||||
|
|
@ -403,7 +403,7 @@ func (fh *FunctionHandler) HandleFuncCall(line preproc.Line) ([]string, error) {
|
|||
|
||||
// Generate final assembly
|
||||
asmLines = append(asmLines, inAssigns...)
|
||||
asmLines = append(asmLines, fmt.Sprintf(" jsr %s", funcName))
|
||||
asmLines = append(asmLines, fmt.Sprintf("\tjsr %s", funcName))
|
||||
asmLines = append(asmLines, outAssigns...)
|
||||
|
||||
return asmLines, nil
|
||||
|
|
@ -451,10 +451,10 @@ func (fh *FunctionHandler) processLabelArg(arg string, param *FuncParam, funcNam
|
|||
}
|
||||
|
||||
*inAssigns = append(*inAssigns,
|
||||
fmt.Sprintf(" lda #<%s", labelName),
|
||||
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf(" lda #>%s", labelName),
|
||||
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda #<%s", labelName),
|
||||
fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda #>%s", labelName),
|
||||
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
|
||||
)
|
||||
|
||||
return nil
|
||||
|
|
@ -475,10 +475,10 @@ func (fh *FunctionHandler) processStringArg(arg string, param *FuncParam, funcNa
|
|||
actualLabel := fh.constStrHandler.AddConstStr(labelName, arg, true, pragmaSet)
|
||||
|
||||
*inAssigns = append(*inAssigns,
|
||||
fmt.Sprintf(" lda #<%s", actualLabel),
|
||||
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf(" lda #>%s", actualLabel),
|
||||
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda #<%s", actualLabel),
|
||||
fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda #>%s", actualLabel),
|
||||
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
|
||||
)
|
||||
|
||||
return nil
|
||||
|
|
@ -492,20 +492,20 @@ func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName
|
|||
// Generate IN assignments (sym -> param)
|
||||
if param.Direction.Has(DirIn) {
|
||||
*inAssigns = append(*inAssigns,
|
||||
fmt.Sprintf(" lda %s", sym.FullName()),
|
||||
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda %s", sym.FullName()),
|
||||
fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
|
||||
)
|
||||
if param.Symbol.IsWord() {
|
||||
if sym.IsWord() {
|
||||
*inAssigns = append(*inAssigns,
|
||||
fmt.Sprintf(" lda %s+1", sym.FullName()),
|
||||
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda %s+1", sym.FullName()),
|
||||
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
|
||||
)
|
||||
} else {
|
||||
// byte -> word: zero extend
|
||||
*inAssigns = append(*inAssigns,
|
||||
" lda #0",
|
||||
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
|
||||
"\tlda #0",
|
||||
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()),
|
||||
)
|
||||
}
|
||||
} else if sym.IsWord() {
|
||||
|
|
@ -518,20 +518,20 @@ func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName
|
|||
// Generate OUT assignments (param -> sym)
|
||||
if param.Direction.Has(DirOut) {
|
||||
*outAssigns = append(*outAssigns,
|
||||
fmt.Sprintf(" lda %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf(" sta %s", sym.FullName()),
|
||||
fmt.Sprintf("\tlda %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tsta %s", sym.FullName()),
|
||||
)
|
||||
if sym.IsWord() {
|
||||
if param.Symbol.IsWord() {
|
||||
*outAssigns = append(*outAssigns,
|
||||
fmt.Sprintf(" lda %s+1", param.Symbol.FullName()),
|
||||
fmt.Sprintf(" sta %s+1", sym.FullName()),
|
||||
fmt.Sprintf("\tlda %s+1", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tsta %s+1", sym.FullName()),
|
||||
)
|
||||
} else {
|
||||
// byte -> word: zero extend
|
||||
*outAssigns = append(*outAssigns,
|
||||
" lda #0",
|
||||
fmt.Sprintf(" sta %s+1", sym.FullName()),
|
||||
"\tlda #0",
|
||||
fmt.Sprintf("\tsta %s+1", sym.FullName()),
|
||||
)
|
||||
}
|
||||
} else if param.Symbol.IsWord() {
|
||||
|
|
@ -576,16 +576,16 @@ func (fh *FunctionHandler) processConstArg(arg string, param *FuncParam, funcNam
|
|||
highByte := uint8((value >> 8) & 0xFF)
|
||||
|
||||
*inAssigns = append(*inAssigns,
|
||||
fmt.Sprintf(" lda #%d", lowByte),
|
||||
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda #%d", lowByte),
|
||||
fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
|
||||
)
|
||||
|
||||
if param.Symbol.IsWord() {
|
||||
// Optimize: only reload A if high byte differs
|
||||
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
|
||||
|
|
@ -604,16 +604,16 @@ func (fh *FunctionHandler) processConstValue(value uint16, param *FuncParam, fun
|
|||
highByte := uint8((value >> 8) & 0xFF)
|
||||
|
||||
*inAssigns = append(*inAssigns,
|
||||
fmt.Sprintf(" lda #%d", lowByte),
|
||||
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
|
||||
fmt.Sprintf("\tlda #%d", lowByte),
|
||||
fmt.Sprintf("\tsta %s", param.Symbol.FullName()),
|
||||
)
|
||||
|
||||
if param.Symbol.IsWord() {
|
||||
// Optimize: only reload A if high byte differs
|
||||
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
|
||||
|
|
@ -917,10 +917,10 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
|
|||
|
||||
// AbsoluteOverlap represents a detected overlap in absolute addresses
|
||||
type AbsoluteOverlap struct {
|
||||
Func1 string // First function using the address
|
||||
Func2 string // Second function using the address
|
||||
Address uint16 // Overlapping address
|
||||
CallChain []string // Call chain from Func1 to Func2
|
||||
Func1 string // First function using the address
|
||||
Func2 string // Second function using the address
|
||||
Address uint16 // Overlapping address
|
||||
CallChain []string // Call chain from Func1 to Func2
|
||||
}
|
||||
|
||||
// AnalyzeAbsoluteOverlaps checks for overlapping absolute addresses in call chains
|
||||
|
|
|
|||
|
|
@ -399,13 +399,13 @@ func TestHandleFuncCall_VarArgs(t *testing.T) {
|
|||
|
||||
// Check generated assembly
|
||||
expectedLines := []string{
|
||||
" lda var_a",
|
||||
" sta test_func_param_a",
|
||||
" lda var_b",
|
||||
" sta test_func_param_b",
|
||||
" lda var_b+1",
|
||||
" sta test_func_param_b+1",
|
||||
" jsr test_func",
|
||||
"\tlda var_a",
|
||||
"\tsta test_func_param_a",
|
||||
"\tlda var_b",
|
||||
"\tsta test_func_param_b",
|
||||
"\tlda var_b+1",
|
||||
"\tsta test_func_param_b+1",
|
||||
"\tjsr test_func",
|
||||
}
|
||||
|
||||
if len(asm) != len(expectedLines) {
|
||||
|
|
@ -928,13 +928,13 @@ func TestHandleFuncCall_AbsoluteParams(t *testing.T) {
|
|||
|
||||
// Check generated assembly uses correct names
|
||||
expectedLines := []string{
|
||||
" lda var_a",
|
||||
" sta test_abs_param_a",
|
||||
" lda var_b",
|
||||
" sta test_abs_param_b",
|
||||
" lda var_b+1",
|
||||
" sta test_abs_param_b+1",
|
||||
" jsr test_abs",
|
||||
"\tlda var_a",
|
||||
"\tsta test_abs_param_a",
|
||||
"\tlda var_b",
|
||||
"\tsta test_abs_param_b",
|
||||
"\tlda var_b+1",
|
||||
"\tsta test_abs_param_b+1",
|
||||
"\tjsr test_abs",
|
||||
}
|
||||
|
||||
if len(asm) != len(expectedLines) {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import "strings"
|
|||
// @@OPT markers are stripped from output.
|
||||
// Returns optimized lines and the set of dissolved REGISTER variable names
|
||||
// (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{}
|
||||
|
||||
if cfg == nil || !cfg.Any() {
|
||||
return lines, dissolved
|
||||
return SourceLineTexts(lines), dissolved
|
||||
}
|
||||
|
||||
parsed := parseLines(lines)
|
||||
|
|
@ -19,6 +19,7 @@ func Optimize(lines []string, cfg *Config) ([]string, map[string]bool) {
|
|||
|
||||
if cfg.EnableStoreLoad {
|
||||
parsed = passStoreReload(parsed, cfg)
|
||||
parsed = passStoreTransfer(parsed, cfg)
|
||||
}
|
||||
if cfg.EnableLoad {
|
||||
parsed = passLoadElimination(parsed, cfg)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,26 @@ import (
|
|||
"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) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
input []SourceLine
|
||||
expected int // expected number of lines after optimization
|
||||
}{
|
||||
{
|
||||
|
|
@ -60,7 +74,7 @@ func TestPassLoadElimination(t *testing.T) {
|
|||
func TestPassImmElimination(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
input []SourceLine
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
|
|
@ -102,7 +116,7 @@ func TestPassImmElimination(t *testing.T) {
|
|||
func TestPassJmpNext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
input []SourceLine
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
|
|
@ -137,7 +151,7 @@ func TestPassJmpNext(t *testing.T) {
|
|||
func TestPassSelfAssignment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
input []SourceLine
|
||||
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) {
|
||||
parsed := parseLines(lines("\tlda RASTER_LINE", "\tlda RASTER_LINE"))
|
||||
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) {
|
||||
input := lines(
|
||||
"\tlda x",
|
||||
|
|
@ -289,7 +331,7 @@ func TestPassStoreReload(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
input []SourceLine
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
|
|
@ -447,13 +489,12 @@ func TestPassStoreReloadIO(t *testing.T) {
|
|||
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
|
||||
// Decimal 53280 = $D020, now caught by IOMap regardless of base
|
||||
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))
|
||||
if len(cleaned) != 2 {
|
||||
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: 0xDC00, End: 0xDC0F},
|
||||
})
|
||||
tests := []struct{
|
||||
addr string
|
||||
tests := []struct {
|
||||
addr string
|
||||
expect int
|
||||
}{
|
||||
{"$D020", 2},
|
||||
|
|
@ -508,7 +549,7 @@ func TestPassStoreReloadIO(t *testing.T) {
|
|||
{"$E000", 1},
|
||||
}
|
||||
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)
|
||||
cleaned := stripOptMarkers(result)
|
||||
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) {
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda #$05",
|
||||
"\tsta myFunc_temp",
|
||||
"\tsta $d020",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -545,12 +691,12 @@ func TestPassRegDead_DeadStore(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPassRegDead_KeptStoreWithLoad(t *testing.T) {
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda #$05",
|
||||
"\tsta myFunc_temp",
|
||||
"\tlda myFunc_temp",
|
||||
"\tsta $d020",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -573,12 +719,12 @@ func TestPassRegDead_KeptStoreWithLoad(t *testing.T) {
|
|||
|
||||
func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
|
||||
// sta regVar before a label — no lda regVar anywhere → globally dead
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda #$05",
|
||||
"\tsta myFunc_temp",
|
||||
"myskip:",
|
||||
"\tnop",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -600,15 +746,15 @@ func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) {
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda #$05",
|
||||
"\tsta normalVar",
|
||||
"\tsta $d020",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
RegisterVars: map[string]bool{},
|
||||
RegisterVars: map[string]bool{},
|
||||
}
|
||||
|
||||
output, _ := Optimize(input, cfg)
|
||||
|
|
@ -620,12 +766,12 @@ func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) {
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda #$05",
|
||||
"\tsta myFunc_temp",
|
||||
"\tsta $d020",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -648,7 +794,7 @@ func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) {
|
|||
|
||||
func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
|
||||
// sta regVar; lda other → A clobbered, but regVar is reloaded later → keep store
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda 53281",
|
||||
"\tsta spill_me_bg",
|
||||
"\tlda 53282",
|
||||
|
|
@ -656,7 +802,7 @@ func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
|
|||
"\tlda spill_me_bg",
|
||||
"\tsta 53282",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -680,14 +826,14 @@ func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
|
|||
|
||||
func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) {
|
||||
// sta → and (A clobbered) → POKE (uses A directly) → rts. Store dead.
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda 56576",
|
||||
"\tsta dissolve_me_temp",
|
||||
"\tand #$fc",
|
||||
"\tsta dissolve_me_temp",
|
||||
"\tsta 56576",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -711,12 +857,12 @@ func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) {
|
|||
func TestPassRegDead_GloballyDeadBeforeJsr(t *testing.T) {
|
||||
// sta regVar; jsr foo; no lda regVar anywhere → globally dead
|
||||
// jsr does not protect a store with zero readers.
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda 53281",
|
||||
"\tsta call_val",
|
||||
"\tjsr helper",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -741,14 +887,14 @@ func TestPassRegDead_JsrProtectsStoreWhenReloaded(t *testing.T) {
|
|||
// sta regVar; jsr foo; lda regVar → store kept
|
||||
// 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.
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda 53281",
|
||||
"\tsta call_val",
|
||||
"\tjsr helper",
|
||||
"\tlda call_val",
|
||||
"\tsta 53280",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -771,13 +917,13 @@ func TestPassRegDead_JsrProtectsStoreWhenReloaded(t *testing.T) {
|
|||
|
||||
func TestPassRegDead_InitValueFlowsThrough(t *testing.T) {
|
||||
// BYTE REGISTER x = 42; POKE $d020, x → value flows through A, never touches RAM
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tlda #$2a",
|
||||
"\tsta test_x",
|
||||
"\tlda test_x",
|
||||
"\tsta 53280",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -805,7 +951,7 @@ func TestPassRegDead_InitValueFlowsThrough(t *testing.T) {
|
|||
func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
|
||||
// sta regVar before a label, no lda regVar anywhere → globally dead
|
||||
// Typical copy-loop pattern: the value flows through A, never reloaded.
|
||||
input := []string{
|
||||
input := lines(
|
||||
"_LOOPSTART",
|
||||
"\tlda (src),y",
|
||||
"\tsta loop_val",
|
||||
|
|
@ -814,7 +960,7 @@ func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
|
|||
"_SKIP:",
|
||||
"\tjmp _LOOPSTART",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -838,7 +984,7 @@ func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
|
|||
func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
|
||||
// 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.
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tsta spill_me_bg",
|
||||
"\tlda 53282",
|
||||
"_SKIP:",
|
||||
|
|
@ -846,7 +992,7 @@ func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
|
|||
"\tlda spill_me_bg",
|
||||
"\tsta 53282",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
EnableRegisterVars: true,
|
||||
|
|
@ -869,181 +1015,181 @@ func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
|
|||
|
||||
func TestPassRegDead_ReadModifyWrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opcode string
|
||||
lines []string
|
||||
name string
|
||||
opcode string
|
||||
lines []SourceLine
|
||||
}{
|
||||
{
|
||||
name: "dec reads stored value",
|
||||
opcode: "dec",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tdec rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "inc reads stored value",
|
||||
opcode: "inc",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tinc rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "adc reads stored value",
|
||||
opcode: "adc",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tclc",
|
||||
"\tadc rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "sbc reads stored value",
|
||||
opcode: "sbc",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tsec",
|
||||
"\tsbc rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "and reads stored value",
|
||||
opcode: "and",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tand rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "ora reads stored value",
|
||||
opcode: "ora",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tora rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "eor reads stored value",
|
||||
opcode: "eor",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\teor rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "cmp reads stored value",
|
||||
opcode: "cmp",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tcmp rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "ldx reads stored value",
|
||||
opcode: "ldx",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tldx rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "ldy reads stored value",
|
||||
opcode: "ldy",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tldy rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "asl reads stored value",
|
||||
opcode: "asl",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tasl rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "lsr reads stored value",
|
||||
opcode: "lsr",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tlsr rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "rol reads stored value",
|
||||
opcode: "rol",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\trol rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "ror reads stored value",
|
||||
opcode: "ror",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tror rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bit reads stored value",
|
||||
opcode: "bit",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tbit rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "cpx reads stored value",
|
||||
opcode: "cpx",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tcpx rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "cpy reads stored value",
|
||||
opcode: "cpy",
|
||||
lines: []string{
|
||||
lines: lines(
|
||||
"\tlda #$05",
|
||||
"\tsta rmw_var",
|
||||
"\tcpy rmw_var",
|
||||
"\trts",
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1074,7 +1220,7 @@ func TestPassRegDead_JmpDoesNotKillStore(t *testing.T) {
|
|||
// sta regVar; ...(then body)...; jmp _END; _ELSE:; ...; _END:; ldy regVar
|
||||
// The jmp in the THEN body should NOT kill the store because
|
||||
// the jump target _END reaches code that reads regVar.
|
||||
input := []string{
|
||||
input := lines(
|
||||
"\tldy #0",
|
||||
"\tlda (zp),y",
|
||||
"\tsta if_val",
|
||||
|
|
@ -1092,7 +1238,7 @@ func TestPassRegDead_JmpDoesNotKillStore(t *testing.T) {
|
|||
"\tldy if_val",
|
||||
"\tlda (zp),y",
|
||||
"\trts",
|
||||
}
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,14 +76,12 @@ func isIndexedOperand(operand string) bool {
|
|||
return strings.Contains(operand, "(") || strings.Contains(operand, ",")
|
||||
}
|
||||
|
||||
// isIOAddr returns true if operand is a hex address marked as I/O in the config.
|
||||
// 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 {
|
||||
if cfg == nil {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(operand, "$") {
|
||||
return false
|
||||
}
|
||||
addr := parseHexOrDec(operand)
|
||||
return addr >= 0 && addr < 65536 && cfg.IOMap[addr]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ func isSafeStldOperand(operand string, cfg *Config) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Check if operand is a direct hex address in an I/O region
|
||||
if strings.HasPrefix(operand, "$") {
|
||||
// Check if operand is a direct numeric address (hex or decimal) in an I/O region.
|
||||
// Symbolic names parse to -1 and are never treated as I/O.
|
||||
if cfg != nil {
|
||||
addr := parseHexOrDec(operand)
|
||||
if addr >= 0 && addr < 65536 && cfg.IOMap[addr] {
|
||||
return false
|
||||
|
|
|
|||
67
internal/optimizer/pass_transfer.go
Normal file
67
internal/optimizer/pass_transfer.go
Normal 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, ""
|
||||
}
|
||||
}
|
||||
|
|
@ -41,9 +41,36 @@ type asmLine struct {
|
|||
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
|
||||
for _, l := range lines {
|
||||
for _, sl := range lines {
|
||||
l := sl.Text
|
||||
al := asmLine{text: l}
|
||||
|
||||
if l == "" {
|
||||
|
|
@ -64,14 +91,21 @@ func parseLines(lines []string) []asmLine {
|
|||
continue
|
||||
}
|
||||
|
||||
if l[0] == '\t' {
|
||||
al.isCode = true
|
||||
// Only compiler-generated, indented lines are treated as optimizable
|
||||
// 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)
|
||||
if len(parts) > 0 {
|
||||
al.isCode = true
|
||||
al.opcode = strings.ToLower(parts[0])
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
al.operand = parts[1]
|
||||
if len(parts) > 1 {
|
||||
al.operand = parts[1]
|
||||
}
|
||||
} else {
|
||||
al.isLabel = true
|
||||
}
|
||||
} else {
|
||||
al.isLabel = true
|
||||
|
|
@ -82,6 +116,12 @@ func parseLines(lines []string) []asmLine {
|
|||
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
|
||||
func stripOptMarkers(lines []asmLine) []asmLine {
|
||||
var result []asmLine
|
||||
|
|
@ -106,4 +146,3 @@ func linesToString(lines []asmLine) []string {
|
|||
func skipJmpMarker(line asmLine) bool {
|
||||
return line.optMarker
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue