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

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

View file

@ -308,7 +308,7 @@ The optimizer never removes **reads or stores** to addresses marked in the I/O e
2. **CLI shorthand**: `--opt-exclude-c64-io` for the full C64 I/O page
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

View file

@ -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
}
@ -103,24 +145,24 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// 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"
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")
codeOutput = append(codeOutput, generatedLine("; ASM block deferred to end of source"))
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
} 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
@ -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
@ -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, "")
}

View file

@ -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

View file

@ -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) {

View file

@ -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)

View file

@ -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)
}
})
}

View file

@ -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]
}

View file

@ -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

View file

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

View file

@ -41,9 +41,36 @@ type asmLine struct {
operand string
}
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
}