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
|
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
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ 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
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,20 +149,20 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
|
|
||||||
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, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,7 +538,7 @@ 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
|
||||||
}{
|
}{
|
||||||
|
|
@ -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,11 +746,11 @@ 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,
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -871,179 +1017,179 @@ 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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
|
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,24 +91,37 @@ 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 {
|
} else {
|
||||||
al.isLabel = true
|
al.isLabel = true
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
al.isLabel = true
|
||||||
|
}
|
||||||
|
|
||||||
result = append(result, al)
|
result = append(result, al)
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue