Added keyword REGISTER for local BYTE variables to help a new optimization mode

This commit is contained in:
Mattias Hansson 2026-07-14 17:19:44 +02:00
parent 11cc5f220b
commit 501f58a968
31 changed files with 850 additions and 126 deletions

View file

@ -5,7 +5,7 @@ for dir in */; do
file="$dir/$name.c65" file="$dir/$name.c65"
if [ -f "$file" ]; then if [ -f "$file" ]; then
echo "=== Building $name ===" echo "=== Building $name ==="
c65gm "$file" c65gm build --opt -i "$file"
echo "" echo ""
fi fi
done done

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="for_byte_max_test" PROGNAME="for_byte_max_test"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="hires" PROGNAME="hires"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -5,7 +5,7 @@ GOTO start
FUNC sethires FUNC sethires
BYTE b BYTE REGISTER b
b = PEEK $d011 b = PEEK $d011
b = b | 32 //enable bitmap mode b = b | 32 //enable bitmap mode
POKE $d011, b POKE $d011, b

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="irq_demo" PROGNAME="irq_demo"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="load_binary_demo" PROGNAME="load_binary_demo"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="memlib_demo" PROGNAME="memlib_demo"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
PROGNAME="memlib_demo2" PROGNAME="memlib_demo2"
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
PROGNAME="multdiv_demo" PROGNAME="multdiv_demo"
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -5,7 +5,7 @@ GOTO start
FUNC setmulti FUNC setmulti
BYTE b BYTE REGISTER b
b = PEEK $d011 b = PEEK $d011
b = b | 32 b = b | 32
POKE $d011, b POKE $d011, b

View file

@ -90,7 +90,7 @@ ENDSCRIPT
// VIC-II multi-color bitmap setup (unchanged from v1) // VIC-II multi-color bitmap setup (unchanged from v1)
//----------------------------------------------------------- //-----------------------------------------------------------
FUNC setmulti FUNC setmulti
BYTE b BYTE REGISTER b
b = PEEK $d011 b = PEEK $d011
b = b | 32 b = b | 32
POKE $d011, b POKE $d011, b

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="script_library_demo" PROGNAME="script_library_demo"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="shift_demo" PROGNAME="shift_demo"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -60,8 +60,8 @@ FEND
//----------------------------------------------------------- //-----------------------------------------------------------
FUNC print_binary({BYTE val}) FUNC print_binary({BYTE val})
BYTE i = 7 BYTE i = 7
BYTE mask BYTE REGISTER mask
BYTE bit BYTE REGISTER bit
lib_cbmio_print(" %") lib_cbmio_print(" %")
@ -139,8 +139,8 @@ FUNC demo_bit_manipulation
// Extract color components from C64 color byte // Extract color components from C64 color byte
// C64 color: bits 7-4 = background, bits 3-0 = foreground // C64 color: bits 7-4 = background, bits 3-0 = foreground
BYTE color = $3E // Background: 3, Foreground: E BYTE color = $3E // Background: 3, Foreground: E
BYTE background BYTE REGISTER background
BYTE foreground BYTE REGISTER foreground
background = color >> 4 background = color >> 4
foreground = color & $0F foreground = color & $0F
@ -159,7 +159,7 @@ FUNC demo_bit_manipulation
// Create bit masks // Create bit masks
lib_cbmio_printlf("bit masks:") lib_cbmio_printlf("bit masks:")
BYTE mask BYTE REGISTER mask
mask = 1 << 0 mask = 1 << 0
lib_cbmio_print("1 << 0 = $") lib_cbmio_print("1 << 0 = $")
@ -287,7 +287,7 @@ FUNC demo_word_operations
lib_cbmio_printlf("") lib_cbmio_printlf("")
// Byte to word conversion with shift // Byte to word conversion with shift
BYTE small = $81 BYTE REGISTER small = $81
WORD large WORD large
large = small << 2 // Zero-extends byte to word, then shifts large = small << 2 // Zero-extends byte to word, then shifts
@ -323,8 +323,8 @@ FUNC demo_c64_example
lib_cbmio_printlf("") lib_cbmio_printlf("")
// Fire button (bit 4) // Fire button (bit 4)
BYTE fire_mask BYTE REGISTER fire_mask
BYTE fire_check BYTE REGISTER fire_check
fire_mask = 1 << 4 fire_mask = 1 << 4
fire_check = joystick & fire_mask fire_check = joystick & fire_mask
IF fire_check = 0 IF fire_check = 0
@ -334,8 +334,8 @@ FUNC demo_c64_example
ENDIF ENDIF
// Up button (bit 0) // Up button (bit 0)
BYTE up_mask BYTE REGISTER up_mask
BYTE up_check BYTE REGISTER up_check
up_mask = 1 << 0 up_mask = 1 << 0
up_check = joystick & up_mask up_check = joystick & up_mask
IF up_check = 0 IF up_check = 0
@ -345,8 +345,8 @@ FUNC demo_c64_example
ENDIF ENDIF
// Right button (bit 3) // Right button (bit 3)
BYTE right_mask BYTE REGISTER right_mask
BYTE right_check BYTE REGISTER right_check
right_mask = 1 << 3 right_mask = 1 << 3
right_check = joystick & right_mask right_check = joystick & right_mask
IF right_check = 0 IF right_check = 0
@ -357,7 +357,7 @@ FUNC demo_c64_example
lib_cbmio_printlf("") lib_cbmio_printlf("")
// Extract direction bits to nibble // Extract direction bits to nibble
BYTE direction BYTE REGISTER direction
direction = joystick & $0F // Mask off fire button direction = joystick & $0F // Mask off fire button
lib_cbmio_print("direction bits: $") lib_cbmio_print("direction bits: $")

View file

@ -2,4 +2,4 @@
# Define filename as variable # Define filename as variable
PROGNAME="switch_demo" PROGNAME="switch_demo"
# Compile and assemble directly # Compile and assemble directly
c65gm ${PROGNAME}.c65 c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -69,8 +69,8 @@ FEND
// Test 3: Nested SWITCH statements // Test 3: Nested SWITCH statements
//----------------------------------------------------------- //-----------------------------------------------------------
FUNC test_nested_switch FUNC test_nested_switch
BYTE outer BYTE REGISTER outer
BYTE inner BYTE REGISTER inner
LET outer = 2 LET outer = 2
LET inner = 3 LET inner = 3
@ -180,8 +180,8 @@ FEND
// Test 7: SWITCH with variable cases (not just literals) // Test 7: SWITCH with variable cases (not just literals)
//----------------------------------------------------------- //-----------------------------------------------------------
FUNC test_variables FUNC test_variables
BYTE match_val1 BYTE REGISTER match_val1
BYTE match_val2 BYTE REGISTER match_val2
WORD match_val3 WORD match_val3
LET match_val1 = 15 LET match_val1 = 15

View file

@ -16,6 +16,8 @@ import (
// BYTE varname = value # byte with init value // BYTE varname = value # byte with init value
// BYTE varname @ address # byte at absolute address // BYTE varname @ address # byte at absolute address
// BYTE CONST varname = value # constant byte // BYTE CONST varname = value # constant byte
// BYTE REGISTER varname # register-hinted byte (function-local only)
// BYTE REGISTER varname = value # register-hinted byte with init value
type ByteCommand struct { type ByteCommand struct {
varName string varName string
value uint16 value uint16
@ -32,7 +34,6 @@ func (c *ByteCommand) WillHandle(line preproc.Line) bool {
} }
func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
// Clear state
c.varName = "" c.varName = ""
c.value = 0 c.value = 0
c.isConst = false c.isConst = false
@ -45,21 +46,32 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
paramCount := len(params) paramCount := len(params)
// Validate parameter count // Check for REGISTER keyword
register := false
if paramCount >= 2 && strings.ToUpper(params[1]) == "REGISTER" {
register = true
params = append(params[:1], params[2:]...)
paramCount = len(params)
}
if register {
if paramCount != 2 && paramCount != 4 {
return fmt.Errorf("BYTE REGISTER: expected 'name' or 'name = value', got %d parameters after REGISTER", paramCount)
}
} else {
if paramCount != 2 && paramCount != 4 && paramCount != 5 { if paramCount != 2 && paramCount != 4 && paramCount != 5 {
return fmt.Errorf("BYTE: wrong number of parameters (%d)", paramCount) return fmt.Errorf("BYTE: wrong number of parameters (%d)", paramCount)
} }
}
var varName string var varName string
var value int64 var value int64
scope := ctx.FunctionHandler.CurrentFunction() scope := ctx.FunctionHandler.CurrentFunction()
// Create constant lookup function
constLookup := ctx.SymbolTable.ConstantLookupFunc(ctx.CurrentScope()) constLookup := ctx.SymbolTable.ConstantLookupFunc(ctx.CurrentScope())
switch paramCount { switch paramCount {
case 2: case 2:
// BYTE varname
varName = params[1] varName = params[1]
value = 0 value = 0
@ -67,10 +79,16 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
return fmt.Errorf("BYTE: invalid identifier %q", varName) return fmt.Errorf("BYTE: invalid identifier %q", varName)
} }
if register {
if scope == "" {
return fmt.Errorf("BYTE REGISTER %q is only valid inside a FUNC block (remove REGISTER or move inside a function)", varName)
}
err = ctx.SymbolTable.AddRegisterVar(varName, scope, uint16(value), line)
} else {
err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line) err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line)
}
case 4: case 4:
// BYTE varname = value OR BYTE varname @ address
varName = params[1] varName = params[1]
operator := params[2] operator := params[2]
valueStr := params[3] valueStr := params[3]
@ -85,14 +103,22 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
} }
if operator == "=" { if operator == "=" {
// BYTE varname = value
if value < 0 || value > 255 { if value < 0 || value > 255 {
return fmt.Errorf("BYTE: init value %d out of range (0-255)", value) return fmt.Errorf("BYTE: init value %d out of range (0-255)", value)
} }
if register {
if scope == "" {
return fmt.Errorf("BYTE REGISTER: variable %q must be declared in function scope", varName)
}
err = ctx.SymbolTable.AddRegisterVar(varName, scope, uint16(value), line)
} else {
err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line) err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line)
}
} else if operator == "@" { } else if operator == "@" {
// BYTE varname @ address if register {
return fmt.Errorf("BYTE REGISTER: @-mapped address is not valid for register variables")
}
if value < 0 || value > 0xFFFF { if value < 0 || value > 0xFFFF {
return fmt.Errorf("BYTE: absolute address $%X out of range", value) return fmt.Errorf("BYTE: absolute address $%X out of range", value)
} }
@ -104,7 +130,9 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
} }
case 5: case 5:
// BYTE CONST varname = value if register {
return fmt.Errorf("BYTE REGISTER: unexpected additional parameters")
}
constKeyword := strings.ToUpper(params[1]) constKeyword := strings.ToUpper(params[1])
varName = params[2] varName = params[2]
operator := params[3] operator := params[3]

View file

@ -438,3 +438,66 @@ func TestByteCommand_ConstantNotFound(t *testing.T) {
t.Errorf("Error should mention constant not found, got: %v", err) t.Errorf("Error should mention constant not found, got: %v", err)
} }
} }
func TestByteCommand_RegisterGlobalScope(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
cmd := &ByteCommand{}
line := preproc.Line{
Text: "BYTE REGISTER x",
Filename: "test.c65",
LineNo: 1,
Kind: preproc.Source,
PragmaSetIndex: 0,
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Expected error for REGISTER in global scope")
}
if !strings.Contains(err.Error(), "REGISTER") {
t.Errorf("Error should mention REGISTER, got: %v", err)
}
}
func TestByteCommand_RegisterAtMapped(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
cmd := &ByteCommand{}
line := preproc.Line{
Text: "BYTE REGISTER x @ $02",
Filename: "test.c65",
LineNo: 1,
Kind: preproc.Source,
PragmaSetIndex: 0,
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Expected error for REGISTER with @-mapped address")
}
if !strings.Contains(err.Error(), "@-mapped") && !strings.Contains(err.Error(), "REGISTER") {
t.Errorf("Error should mention @-mapped issue, got: %v", err)
}
}
func TestByteCommand_RegisterConst(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
cmd := &ByteCommand{}
line := preproc.Line{
Text: "BYTE REGISTER CONST x = 5",
Filename: "test.c65",
LineNo: 1,
Kind: preproc.Source,
PragmaSetIndex: 0,
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Expected error for REGISTER CONST")
}
}

View file

@ -47,6 +47,11 @@ func (c *WordCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
paramCount := len(params) paramCount := len(params)
// Reject WORD REGISTER (6502 has no 16-bit ALU register)
if paramCount >= 2 && strings.ToUpper(params[1]) == "REGISTER" {
return fmt.Errorf("WORD REGISTER is not supported; only BYTE variables may use the REGISTER hint (the 6502 has no 16-bit ALU register). Use a @-mapped zero-page WORD instead")
}
// Validate parameter count // Validate parameter count
if paramCount != 2 && paramCount != 4 && paramCount != 5 { if paramCount != 2 && paramCount != 4 && paramCount != 5 {
return fmt.Errorf("WORD: wrong number of parameters (%d)", paramCount) return fmt.Errorf("WORD: wrong number of parameters (%d)", paramCount)

View file

@ -636,3 +636,25 @@ func TestWordCommand_MultipleStrings(t *testing.T) {
t.Errorf("Expected at least 9 lines of string declarations, got %d", len(strDecls)) t.Errorf("Expected at least 9 lines of string declarations, got %d", len(strDecls))
} }
} }
func TestWordCommand_RegisterError(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
cmd := &WordCommand{}
line := preproc.Line{
Text: "WORD REGISTER x",
Filename: "test.c65",
LineNo: 1,
Kind: preproc.Source,
PragmaSetIndex: 0,
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Expected error for WORD REGISTER")
}
if !strings.Contains(err.Error(), "REGISTER") {
t.Errorf("Error should mention REGISTER, got: %v", err)
}
}

View file

@ -15,6 +15,7 @@ type Compiler struct {
ctx *CompilerContext ctx *CompilerContext
registry *CommandRegistry registry *CommandRegistry
deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma
dissolvedVars map[string]bool // REGISTER vars dissolved by optimizer
CmdlineOpt bool // --opt enables all passes CmdlineOpt bool // --opt enables all passes
CmdlineDebug bool // --opt-debug enables debug output CmdlineDebug bool // --opt-debug enables debug output
CmdlineIORegions []optimizer.IORegion // --opt-exclude ranges CmdlineIORegions []optimizer.IORegion // --opt-exclude ranges
@ -197,6 +198,11 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} }
varName := codePart[start+1 : end] varName := codePart[start+1 : end]
sym := c.ctx.SymbolTable.LookupWithoutUsage(varName, c.ctx.CurrentScope())
if sym != nil && sym.IsRegister() {
c.printErrorWithContext(lines, i, fmt.Errorf("REGISTER variable %q cannot be referenced from ASM blocks", varName))
return nil, fmt.Errorf("compilation failed")
}
expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope()) expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope())
codePart = codePart[:start] + expandedName + codePart[end+1:] codePart = codePart[:start] + expandedName + codePart[end+1:]
// Continue searching after the replacement // Continue searching after the replacement
@ -271,7 +277,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Peephole optimization pass // Peephole optimization pass
if cfg := c.getOptimizerConfig(); cfg != nil { if cfg := c.getOptimizerConfig(); cfg != nil {
codeOutput = optimizer.Optimize(codeOutput, cfg) var dissolved map[string]bool
codeOutput, dissolved = optimizer.Optimize(codeOutput, cfg)
c.dissolvedVars = dissolved
} }
// Analyze for overlapping absolute addresses in function call chains // Analyze for overlapping absolute addresses in function call chains
@ -281,7 +289,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma() funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma()
// Check for unused variables and print warnings (skip variables in functions with remove pragma) // Check for unused variables and print warnings (skip variables in functions with remove pragma)
warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma) warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma, c.dissolvedVars)
for _, warning := range warnings { for _, warning := range warnings {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning) _, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
} }
@ -342,11 +350,20 @@ func (c *Compiler) getOptimizerConfig() *optimizer.Config {
cfg.EnableJmp = true cfg.EnableJmp = true
cfg.EnableSelf = true cfg.EnableSelf = true
cfg.EnableStoreLoad = true cfg.EnableStoreLoad = true
cfg.EnableRegisterVars = true
} }
if c.CmdlineDebug { if c.CmdlineDebug {
cfg.Debug = true cfg.Debug = true
} }
// Populate register variable names for the optimizer
cfg.RegisterVars = make(map[string]bool)
for _, sym := range c.ctx.SymbolTable.Symbols() {
if sym.IsRegister() {
cfg.RegisterVars[sym.FullName()] = true
}
}
if !cfg.Any() { if !cfg.Any() {
return nil return nil
} }
@ -724,7 +741,7 @@ func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bo
output = append(output, "") output = append(output, "")
// Variables section // Variables section
if varLines := GenerateVariables(c.ctx.SymbolTable, removedFuncs); len(varLines) > 0 { if varLines := GenerateVariables(c.ctx.SymbolTable, removedFuncs, c.dissolvedVars); len(varLines) > 0 {
output = append(output, varLines...) output = append(output, varLines...)
} }

View file

@ -159,6 +159,11 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) (string, error) {
return "", fmt.Errorf("%s:%d: FUNC %s: parameter %q cannot be a constant", line.Filename, line.LineNo, funcName, varName) return "", fmt.Errorf("%s:%d: FUNC %s: parameter %q cannot be a constant", line.Filename, line.LineNo, funcName, varName)
} }
if sym.IsRegister() && direction.Has(DirOut) {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return "", fmt.Errorf("%s:%d: FUNC %s: REGISTER parameter %q cannot be out: or io: (REGISTER values do not persist after the function call)", line.Filename, line.LineNo, funcName, varName)
}
funcParams = append(funcParams, &FuncParam{ funcParams = append(funcParams, &FuncParam{
Symbol: sym, Symbol: sym,
Direction: direction, Direction: direction,
@ -614,15 +619,26 @@ func (fh *FunctionHandler) processConstValue(value uint16, param *FuncParam, fun
return nil return nil
} }
// parseImplicitDecl parses {BYTE varname} or {WORD varname} or {BYTE varname @ address} and adds to symbol table // parseImplicitDecl parses {BYTE varname} or {WORD varname} or {BYTE REGISTER varname} or {BYTE varname @ address} and adds to symbol table
func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line preproc.Line) error { func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line preproc.Line) error {
parts := strings.Fields(decl) parts := strings.Fields(decl)
if len(parts) != 2 && len(parts) != 4 { if len(parts) < 2 || len(parts) > 4 {
return fmt.Errorf("implicit declaration must be 'TYPE name' or 'TYPE name @ addr', got: %q", decl) return fmt.Errorf("implicit declaration must be 'TYPE name', 'TYPE REGISTER name', or 'TYPE name @ addr', got: %q", decl)
} }
typeIdx := 0
typeStr := strings.ToUpper(parts[0]) typeStr := strings.ToUpper(parts[0])
varName := parts[1] register := false
// Check for REGISTER keyword after type
if len(parts) >= 3 && strings.ToUpper(parts[1]) == "REGISTER" {
register = true
typeIdx = 1 // parts[1] consumed as REGISTER
}
if register && typeStr == "WORD" {
return fmt.Errorf("WORD REGISTER is not supported; only BYTE variables may use the REGISTER hint")
}
var kind VarKind var kind VarKind
switch typeStr { switch typeStr {
@ -634,23 +650,35 @@ func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line
return fmt.Errorf("implicit declaration type must be BYTE or WORD, got: %s", typeStr) return fmt.Errorf("implicit declaration type must be BYTE or WORD, got: %s", typeStr)
} }
if len(parts) == 2 { if register && kind != KindByte {
// Simple: BYTE name or WORD name return fmt.Errorf("REGISTER hint is only valid for BYTE variables")
}
varName := parts[1+typeIdx]
// Simple declaration: TYPE [REGISTER] name
if len(parts) == 2+typeIdx {
if register {
return fh.symTable.AddRegisterVar(varName, funcName, 0, line)
}
return fh.symTable.AddVar(varName, funcName, kind, 0, line) return fh.symTable.AddVar(varName, funcName, kind, 0, line)
} }
// Extended: BYTE name @ address or WORD name @ address // Extended: TYPE [REGISTER] name @ address
operator := parts[2] if len(parts) == 4+typeIdx {
addrStr := parts[3] operator := parts[2+typeIdx]
addrStr := parts[3+typeIdx]
if register {
return fmt.Errorf("REGISTER variable cannot be @-mapped; REGISTER is incompatible with fixed address")
}
if operator != "@" { if operator != "@" {
return fmt.Errorf("expected '@' operator, got: %q", operator) return fmt.Errorf("expected '@' operator, got: %q", operator)
} }
// Create constant lookup function for address evaluation
constLookup := fh.symTable.ConstantLookupFunc([]string{funcName}) constLookup := fh.symTable.ConstantLookupFunc([]string{funcName})
// Parse address (supports $hex and decimal) using EvaluateExpression
addr, err := utils.EvaluateExpression(addrStr, constLookup) addr, err := utils.EvaluateExpression(addrStr, constLookup)
if err != nil { if err != nil {
return fmt.Errorf("invalid address %q: %w", addrStr, err) return fmt.Errorf("invalid address %q: %w", addrStr, err)
@ -661,6 +689,9 @@ func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line
} }
return fh.symTable.AddAbsolute(varName, funcName, kind, uint16(addr), line) return fh.symTable.AddAbsolute(varName, funcName, kind, uint16(addr), line)
}
return fmt.Errorf("invalid implicit declaration format: %q", decl)
} }
// EndFunction pops all functions from the stack (called by FEND) // EndFunction pops all functions from the stack (called by FEND)
@ -863,7 +894,7 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
} }
} }
// Check for implicit declaration {TYPE name} // Check for implicit declaration {TYPE name} or {TYPE REGISTER name}
if strings.HasPrefix(varName, "{") && strings.HasSuffix(varName, "}") { if strings.HasPrefix(varName, "{") && strings.HasSuffix(varName, "}") {
isImplicit = true isImplicit = true
implicitDecl = varName[1 : len(varName)-1] // strip { } implicitDecl = varName[1 : len(varName)-1] // strip { }
@ -873,8 +904,13 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
if len(parts) < 2 { if len(parts) < 2 {
return 0, "", false, "", fmt.Errorf("invalid implicit declaration: %q", varName) return 0, "", false, "", fmt.Errorf("invalid implicit declaration: %q", varName)
} }
// Handle {TYPE REGISTER name} — variable name is parts[2]
if len(parts) >= 3 && strings.ToUpper(parts[1]) == "REGISTER" {
varName = parts[2]
} else {
varName = parts[1] varName = parts[1]
} }
}
return direction, varName, isImplicit, implicitDecl, nil return direction, varName, isImplicit, implicitDecl, nil
} }

View file

@ -1958,3 +1958,91 @@ func TestMultiFuncGroupRemoval(t *testing.T) {
} }
}) })
} }
func TestParseImplicitDecl_Register(t *testing.T) {
pragma := preproc.NewPragma()
symTable := NewSymbolTable()
fh := NewFunctionHandler(symTable, NewLabelStack("L"), nil, pragma)
err := fh.parseImplicitDecl("BYTE REGISTER temp", "myFunc", preproc.Line{Filename: "test.c65", LineNo: 1})
if err != nil {
t.Fatalf("parseImplicitDecl with REGISTER failed: %v", err)
}
sym := symTable.Lookup("temp", []string{"myFunc"})
if sym == nil {
t.Fatal("expected symbol temp to be found")
}
if !sym.IsRegister() {
t.Error("expected IsRegister() to be true")
}
}
func TestParseImplicitDecl_WordRegisterError(t *testing.T) {
pragma := preproc.NewPragma()
symTable := NewSymbolTable()
fh := NewFunctionHandler(symTable, NewLabelStack("L"), nil, pragma)
err := fh.parseImplicitDecl("WORD REGISTER ptr", "myFunc", preproc.Line{Filename: "test.c65", LineNo: 1})
if err == nil {
t.Fatal("expected error for WORD REGISTER")
}
if !strings.Contains(err.Error(), "REGISTER") {
t.Errorf("error should mention REGISTER, got: %v", err)
}
}
func TestParseImplicitDecl_RegisterAtError(t *testing.T) {
pragma := preproc.NewPragma()
symTable := NewSymbolTable()
fh := NewFunctionHandler(symTable, NewLabelStack("L"), nil, pragma)
err := fh.parseImplicitDecl("BYTE REGISTER temp @ $FB", "myFunc", preproc.Line{Filename: "test.c65", LineNo: 1})
if err == nil {
t.Fatal("expected error for REGISTER with @")
}
}
func TestHandleFuncDecl_OutRegisterError(t *testing.T) {
st := NewSymbolTable()
ls := NewLabelStack("L")
csh := NewConstantStringHandler()
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
_, err := fh.HandleFuncDecl(makeLine("FUNC test_out_reg ( out:{BYTE REGISTER temp} )"))
if err == nil {
t.Fatal("expected error for out: REGISTER parameter")
}
if !strings.Contains(err.Error(), "REGISTER") && !strings.Contains(err.Error(), "out:") {
t.Errorf("error should mention REGISTER and out:, got: %v", err)
}
}
func TestHandleFuncDecl_ImplicitRegisterParam(t *testing.T) {
st := NewSymbolTable()
ls := NewLabelStack("L")
csh := NewConstantStringHandler()
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
funcName, err := fh.HandleFuncDecl(makeLine("FUNC test_reg ( {BYTE REGISTER temp} )"))
if err != nil {
t.Fatalf("HandleFuncDecl with REGISTER failed: %v", err)
}
if funcName != "test_reg" {
t.Fatalf("expected funcName = \"test_reg\", got %q", funcName)
}
sym := st.Lookup("temp", []string{"test_reg"})
if sym == nil {
t.Fatal("REGISTER parameter not declared")
}
if !sym.IsRegister() {
t.Error("expected IsRegister() to be true")
}
if !sym.IsByte() {
t.Error("expected IsByte() to be true")
}
}

View file

@ -214,7 +214,10 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
scriptText := strings.Join(texts, "\n") scriptText := strings.Join(texts, "\n")
// Expand |varname| -> actual variable names // Expand |varname| -> actual variable names
scriptText = expandVariables(scriptText, ctx) scriptText, err := expandVariables(scriptText, ctx)
if err != nil {
return nil, err
}
// Determine the source filename for Starlark // Determine the source filename for Starlark
sourceFile := scriptLines[0].Filename sourceFile := scriptLines[0].Filename
@ -298,7 +301,7 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
} }
// expandVariables replaces |varname| with expanded variable names from symbol table // expandVariables replaces |varname| with expanded variable names from symbol table
func expandVariables(text string, ctx *CompilerContext) string { func expandVariables(text string, ctx *CompilerContext) (string, error) {
result := text result := text
for { for {
start := strings.IndexByte(result, '|') start := strings.IndexByte(result, '|')
@ -312,10 +315,14 @@ func expandVariables(text string, ctx *CompilerContext) string {
end += start + 1 end += start + 1
varName := result[start+1 : end] varName := result[start+1 : end]
sym := ctx.SymbolTable.LookupWithoutUsage(varName, ctx.CurrentScope())
if sym != nil && sym.IsRegister() {
return "", fmt.Errorf("REGISTER variable %q cannot be referenced from SCRIPT/MACRO blocks", varName)
}
expandedName := ctx.SymbolTable.ExpandName(varName, ctx.CurrentScope()) expandedName := ctx.SymbolTable.ExpandName(varName, ctx.CurrentScope())
result = result[:start] + expandedName + result[end+1:] result = result[:start] + expandedName + result[end+1:]
} }
return result return result, nil
} }
// validateScriptFilePath checks that path is safe and resolves it within the project root. // validateScriptFilePath checks that path is safe and resolves it within the project root.
@ -547,7 +554,10 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaS
// Expand |varname| -> actual variable names in the OUTPUT // Expand |varname| -> actual variable names in the OUTPUT
// This happens at call site, so local variables are resolved using caller's scope // This happens at call site, so local variables are resolved using caller's scope
outputStr = expandVariables(outputStr, ctx) outputStr, err = expandVariables(outputStr, ctx)
if err != nil {
return nil, err
}
return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil
} }

View file

@ -26,6 +26,7 @@ const (
FlagAbsolute FlagAbsolute
FlagZeroPage FlagZeroPage
FlagLabelRef FlagLabelRef
FlagRegister
) )
// Symbol represents a variable, constant, or label reference // Symbol represents a variable, constant, or label reference
@ -74,6 +75,7 @@ func (s *Symbol) IsWord() bool { return s.Has(FlagWord) }
func (s *Symbol) IsConst() bool { return s.Has(FlagConst) } func (s *Symbol) IsConst() bool { return s.Has(FlagConst) }
func (s *Symbol) IsAbsolute() bool { return s.Has(FlagAbsolute) } func (s *Symbol) IsAbsolute() bool { return s.Has(FlagAbsolute) }
func (s *Symbol) IsZeroPage() bool { return s.Has(FlagZeroPage) } func (s *Symbol) IsZeroPage() bool { return s.Has(FlagZeroPage) }
func (s *Symbol) IsRegister() bool { return s.Has(FlagRegister) }
func (s *Symbol) IsZeroPagePointer() bool { return s.HasAll(FlagAbsolute | FlagZeroPage | FlagWord) } func (s *Symbol) IsZeroPagePointer() bool { return s.HasAll(FlagAbsolute | FlagZeroPage | FlagWord) }
// FullName returns the fully qualified name (scope.name or just name) // FullName returns the fully qualified name (scope.name or just name)
@ -143,6 +145,20 @@ func (st *SymbolTable) AddVar(name, scope string, kind VarKind, initValue uint16
}) })
} }
// AddRegisterVar adds a REGISTER-hinted byte variable (function-local only, BYTE only)
func (st *SymbolTable) AddRegisterVar(name, scope string, initValue uint16, line preproc.Line) error {
if scope == "" {
return fmt.Errorf("BYTE REGISTER %q is only valid inside a FUNC block (remove REGISTER or move inside a function)", name)
}
return st.add(&Symbol{
Name: name,
Scope: scope,
Flags: FlagByte | FlagRegister,
Value: initValue,
Line: line,
})
}
// AddConst adds a constant (byte or word) // AddConst adds a constant (byte or word)
func (st *SymbolTable) AddConst(name, scope string, kind VarKind, value uint16, line preproc.Line) error { func (st *SymbolTable) AddConst(name, scope string, kind VarKind, value uint16, line preproc.Line) error {
var flags SymbolFlags var flags SymbolFlags
@ -340,15 +356,13 @@ func (st *SymbolTable) ConstantLookupFunc(currentScopes []string) func(string) (
// CheckUnused returns warnings for unused variables // CheckUnused returns warnings for unused variables
// Returns slice of warning messages for regular variables (not constants, not absolutes) that were never used // Returns slice of warning messages for regular variables (not constants, not absolutes) that were never used
// excludeFuncs is a map of function names that will be removed (e.g., have _P_REMOVE_UNUSED pragma) // excludeFuncs is a map of function names that will be removed (e.g., have _P_REMOVE_UNUSED pragma)
func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string { // dissolvedVars is a set of REGISTER variables dissolved by the optimizer (do not warn)
func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool, dissolvedVars map[string]bool) []string {
var warnings []string var warnings []string
for _, sym := range st.symbols { for _, sym := range st.symbols {
// Skip constants and absolute variables (they shouldn't track usage) // Skip constants and absolute variables (they shouldn't track usage)
if sym.IsConst() || sym.IsAbsolute() { if sym.IsConst() || sym.IsAbsolute() {
// Sanity check: constants and absolutes should never be marked as used
// If they are, it's a bug in the compiler
if sym.IsUsed() { if sym.IsUsed() {
// This would be an internal error, but we'll just skip it
continue continue
} }
continue continue
@ -359,6 +373,11 @@ func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string {
continue continue
} }
// Skip dissolved REGISTER variables (dissolved is the intended outcome)
if sym.IsRegister() && dissolvedVars != nil && dissolvedVars[sym.FullName()] {
continue
}
// Check if pragma indicates we should ignore unused warnings for this variable // Check if pragma indicates we should ignore unused warnings for this variable
if st.pragma != nil { if st.pragma != nil {
pragmaSet := st.pragma.GetPragmaSetByIndex(sym.Line.PragmaSetIndex) pragmaSet := st.pragma.GetPragmaSetByIndex(sym.Line.PragmaSetIndex)
@ -370,7 +389,6 @@ func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string {
// Check if variable was never used // Check if variable was never used
if !sym.IsUsed() { if !sym.IsUsed() {
// Format warning message with file and line info
var scopeInfo string var scopeInfo string
if sym.Scope != "" { if sym.Scope != "" {
scopeInfo = fmt.Sprintf(" in function '%s'", sym.Scope) scopeInfo = fmt.Sprintf(" in function '%s'", sym.Scope)
@ -502,7 +520,7 @@ func GenerateAbsolutes(st *SymbolTable, excludeScopes map[string]bool) []string
} }
// GenerateVariables generates variable declarations (name !8 $value) // GenerateVariables generates variable declarations (name !8 $value)
func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool) []string { func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool, dissolvedVars map[string]bool) []string {
var lines []string var lines []string
hasVars := false hasVars := false
@ -515,6 +533,10 @@ func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool) []string
if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] { if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] {
continue continue
} }
// Skip dissolved REGISTER variables (optimizer eliminated all references)
if sym.IsRegister() && dissolvedVars != nil && dissolvedVars[sym.FullName()] {
continue
}
hasVars = true hasVars = true
var line string var line string

View file

@ -580,7 +580,7 @@ func TestGenerateVariables(t *testing.T) {
// Absolute (should be skipped) // Absolute (should be skipped)
st.AddAbsolute("SKIP2", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddAbsolute("SKIP2", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
lines := GenerateVariables(st, nil) lines := GenerateVariables(st, nil, nil)
if len(lines) == 0 { if len(lines) == 0 {
t.Fatal("expected output lines") t.Fatal("expected output lines")
@ -630,7 +630,7 @@ func TestGenerateEmpty(t *testing.T) {
if lines := GenerateAbsolutes(st, nil); lines != nil { if lines := GenerateAbsolutes(st, nil); lines != nil {
t.Error("expected nil for empty absolutes") t.Error("expected nil for empty absolutes")
} }
if lines := GenerateVariables(st, nil); lines != nil { if lines := GenerateVariables(st, nil, nil); lines != nil {
t.Error("expected nil for empty variables") t.Error("expected nil for empty variables")
} }
@ -652,7 +652,7 @@ func TestGenerateScopedVariables(t *testing.T) {
st.AddVar("local", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("local", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("nested", "main_helper", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("nested", "main_helper", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
lines := GenerateVariables(st, nil) lines := GenerateVariables(st, nil, nil)
output := strings.Join(lines, "\n") output := strings.Join(lines, "\n")
// Check full names are used // Check full names are used
@ -676,7 +676,7 @@ func TestGenerateHexLowercase(t *testing.T) {
constLines := GenerateConstants(st, nil) constLines := GenerateConstants(st, nil)
absLines := GenerateAbsolutes(st, nil) absLines := GenerateAbsolutes(st, nil)
varLines := GenerateVariables(st, nil) varLines := GenerateVariables(st, nil, nil)
output := strings.Join(append(append(constLines, absLines...), varLines...), "\n") output := strings.Join(append(append(constLines, absLines...), varLines...), "\n")
@ -716,7 +716,7 @@ func TestUsageTracking(t *testing.T) {
} }
// Check that warning is generated // Check that warning is generated
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 1 { if len(warnings) != 1 {
t.Fatalf("CheckUnused() returned %d warnings, want 1", len(warnings)) t.Fatalf("CheckUnused() returned %d warnings, want 1", len(warnings))
} }
@ -748,7 +748,7 @@ func TestUsageTracking(t *testing.T) {
} }
// Check that no warning is generated // Check that no warning is generated
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 0 { if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings, want 0: %v", len(warnings), warnings) t.Errorf("CheckUnused() returned %d warnings, want 0: %v", len(warnings), warnings)
} }
@ -764,7 +764,7 @@ func TestUsageTracking(t *testing.T) {
} }
// Check that no warning is generated // Check that no warning is generated
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 0 { if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for constant, want 0: %v", len(warnings), warnings) t.Errorf("CheckUnused() returned %d warnings for constant, want 0: %v", len(warnings), warnings)
} }
@ -786,7 +786,7 @@ func TestUsageTracking(t *testing.T) {
} }
// Check that no warning is generated // Check that no warning is generated
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 0 { if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for absolute variable, want 0: %v", len(warnings), warnings) t.Errorf("CheckUnused() returned %d warnings for absolute variable, want 0: %v", len(warnings), warnings)
} }
@ -821,7 +821,7 @@ func TestUsageTracking(t *testing.T) {
st.Lookup("local_used", []string{"myFunc"}) st.Lookup("local_used", []string{"myFunc"})
// Check warnings // Check warnings
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 2 { if len(warnings) != 2 {
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings) t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
} }
@ -873,7 +873,7 @@ func TestUsageTracking(t *testing.T) {
} }
// No warnings should be generated // No warnings should be generated
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 0 { if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for used variable, want 0", len(warnings)) t.Errorf("CheckUnused() returned %d warnings for used variable, want 0", len(warnings))
} }
@ -900,7 +900,7 @@ func TestUsageTracking(t *testing.T) {
} }
// Warning should be generated // Warning should be generated
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 1 { if len(warnings) != 1 {
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings)) t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
} }
@ -922,7 +922,7 @@ func TestUsageTracking(t *testing.T) {
st.Lookup("used2", []string{"func1"}) st.Lookup("used2", []string{"func1"})
// Check warnings // Check warnings
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 2 { if len(warnings) != 2 {
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings) t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
} }
@ -970,7 +970,7 @@ func TestUsageTracking(t *testing.T) {
} }
// Since we haven't used it, it should generate a warning // Since we haven't used it, it should generate a warning
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 1 { if len(warnings) != 1 {
t.Errorf("CheckUnused() returned %d warnings for label reference, want 1", len(warnings)) t.Errorf("CheckUnused() returned %d warnings for label reference, want 1", len(warnings))
} }
@ -979,7 +979,7 @@ func TestUsageTracking(t *testing.T) {
st.Lookup("handler", []string{}) st.Lookup("handler", []string{})
// Now no warning should be generated // Now no warning should be generated
warnings = st.CheckUnused(nil) warnings = st.CheckUnused(nil, nil)
if len(warnings) != 0 { if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for used label reference, want 0", len(warnings)) t.Errorf("CheckUnused() returned %d warnings for used label reference, want 0", len(warnings))
} }
@ -1022,7 +1022,7 @@ func TestUsageTracking(t *testing.T) {
} }
// Warning should be generated for the variable // Warning should be generated for the variable
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 1 { if len(warnings) != 1 {
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings)) t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
} }
@ -1067,9 +1067,101 @@ func TestUsageTracking(t *testing.T) {
} }
// No warnings should be generated // No warnings should be generated
warnings := st.CheckUnused(nil) warnings := st.CheckUnused(nil, nil)
if len(warnings) != 0 { if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for constants/absolutes, want 0", len(warnings)) t.Errorf("CheckUnused() returned %d warnings for constants/absolutes, want 0", len(warnings))
} }
}) })
} }
func TestAddRegisterVar(t *testing.T) {
st := NewSymbolTable()
err := st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
if err != nil {
t.Fatalf("AddRegisterVar failed: %v", err)
}
sym := st.Lookup("temp", []string{"myFunc"})
if sym == nil {
t.Fatal("expected symbol to be found")
}
if !sym.IsRegister() {
t.Error("expected IsRegister() to be true")
}
if !sym.IsByte() {
t.Error("expected IsByte() to be true")
}
if sym.FullName() != "myFunc_temp" {
t.Errorf("expected full name myFunc_temp, got %q", sym.FullName())
}
}
func TestAddRegisterVar_GlobalScopeError(t *testing.T) {
st := NewSymbolTable()
err := st.AddRegisterVar("temp", "", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
if err == nil {
t.Fatal("expected error for global scope REGISTER")
}
if !strings.Contains(err.Error(), "REGISTER") {
t.Errorf("error should mention REGISTER, got: %v", err)
}
}
func TestGenerateVariables_DissolvedRegister(t *testing.T) {
st := NewSymbolTable()
st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("normal", "myFunc", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
dissolved := map[string]bool{"myFunc_temp": true}
lines := GenerateVariables(st, nil, dissolved)
joined := strings.Join(lines, "\n")
if strings.Contains(joined, "myFunc_temp") {
t.Error("dissolved REGISTER variable should not be emitted")
}
if !strings.Contains(joined, "myFunc_normal") {
t.Error("normal variable should still be emitted")
}
}
func TestGenerateVariables_NonDissolvedRegister(t *testing.T) {
st := NewSymbolTable()
st.AddRegisterVar("temp", "myFunc", 42, preproc.Line{Filename: "test.c65", LineNo: 1})
lines := GenerateVariables(st, nil, nil)
joined := strings.Join(lines, "\n")
if !strings.Contains(joined, "myFunc_temp") {
t.Error("non-dissolved REGISTER variable should be emitted with !8")
}
if !strings.Contains(joined, "!8 $2a") {
t.Error("expected init value 42 ($2a) in output")
}
}
func TestCheckUnused_DissolvedRegisterNoWarning(t *testing.T) {
st := NewSymbolTable()
st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
dissolved := map[string]bool{"myFunc_temp": true}
warnings := st.CheckUnused(nil, dissolved)
if len(warnings) != 0 {
t.Errorf("dissolved REGISTER var should not trigger warning, got: %v", warnings)
}
}
func TestCheckUnused_NonDissolvedRegisterWarning(t *testing.T) {
st := NewSymbolTable()
st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
warnings := st.CheckUnused(nil, nil)
if len(warnings) != 1 {
t.Errorf("non-dissolved unused REGISTER var should trigger 1 warning, got %d", len(warnings))
}
}

View file

@ -14,9 +14,11 @@ type Config struct {
EnableJmp bool EnableJmp bool
EnableSelf bool EnableSelf bool
EnableStoreLoad bool EnableStoreLoad bool
EnableRegisterVars bool
Debug bool Debug bool
ShowMarkers bool ShowMarkers bool
IOMap [65536]bool IOMap [65536]bool
RegisterVars map[string]bool
} }
func NewConfig(ps preproc.PragmaSet) *Config { func NewConfig(ps preproc.PragmaSet) *Config {
@ -28,13 +30,14 @@ func NewConfig(ps preproc.PragmaSet) *Config {
EnableJmp: all || (ps.GetPragma("_P_OPT_JMP") != "" && ps.GetPragma("_P_OPT_JMP") != "0"), EnableJmp: all || (ps.GetPragma("_P_OPT_JMP") != "" && ps.GetPragma("_P_OPT_JMP") != "0"),
EnableSelf: all || (ps.GetPragma("_P_OPT_SELF") != "" && ps.GetPragma("_P_OPT_SELF") != "0"), EnableSelf: all || (ps.GetPragma("_P_OPT_SELF") != "" && ps.GetPragma("_P_OPT_SELF") != "0"),
EnableStoreLoad: all || (ps.GetPragma("_P_OPT_STLD") != "" && ps.GetPragma("_P_OPT_STLD") != "0"), EnableStoreLoad: all || (ps.GetPragma("_P_OPT_STLD") != "" && ps.GetPragma("_P_OPT_STLD") != "0"),
EnableRegisterVars: all || (ps.GetPragma("_P_OPT_REGISTER_VARS") != "" && ps.GetPragma("_P_OPT_REGISTER_VARS") != "0"),
Debug: (ps.GetPragma("_P_OPT_DEBUG") != "" && ps.GetPragma("_P_OPT_DEBUG") != "0"), Debug: (ps.GetPragma("_P_OPT_DEBUG") != "" && ps.GetPragma("_P_OPT_DEBUG") != "0"),
ShowMarkers: (ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"), ShowMarkers: (ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"),
} }
} }
func (c *Config) Any() bool { func (c *Config) Any() bool {
return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf || c.EnableStoreLoad return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf || c.EnableStoreLoad || c.EnableRegisterVars
} }
// BuildIOMap scans all pragma sets for _P_OPT_IO and marks I/O regions. // BuildIOMap scans all pragma sets for _P_OPT_IO and marks I/O regions.

View file

@ -1,11 +1,17 @@
package optimizer package optimizer
import "strings"
// Optimize applies all enabled peephole passes to the generated ASM lines. // Optimize applies all enabled peephole passes to the generated ASM lines.
// Each pass runs sequentially on a parsed representation of the lines. // Each pass runs sequentially on a parsed representation of the lines.
// @@OPT markers are stripped from output. // @@OPT markers are stripped from output.
func Optimize(lines []string, cfg *Config) []string { // 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) {
dissolved := map[string]bool{}
if cfg == nil || !cfg.Any() { if cfg == nil || !cfg.Any() {
return lines return lines, dissolved
} }
parsed := parseLines(lines) parsed := parseLines(lines)
@ -26,11 +32,41 @@ func Optimize(lines []string, cfg *Config) []string {
if cfg.EnableSelf { if cfg.EnableSelf {
parsed = passSelfAssignment(parsed) parsed = passSelfAssignment(parsed)
} }
if cfg.EnableRegisterVars && len(cfg.RegisterVars) > 0 {
parsed = passRegDead(parsed, cfg.RegisterVars)
}
parsed = stripOptMarkers(parsed) parsed = stripOptMarkers(parsed)
if cfg.Debug { if cfg.Debug {
original = stripOptMarkers(original) original = stripOptMarkers(original)
parsed = debugDiff(original, parsed) parsed = debugDiff(original, parsed)
} }
return linesToString(parsed)
resultLines := linesToString(parsed)
if len(cfg.RegisterVars) > 0 {
dissolved = computeDissolved(resultLines, cfg.RegisterVars)
}
return resultLines, dissolved
}
// computeDissolved finds REGISTER variables that have no remaining references
// in the output and can have their memory allocation elided.
func computeDissolved(outputLines []string, registerVars map[string]bool) map[string]bool {
dissolved := map[string]bool{}
for name := range registerVars {
dissolved[name] = true
}
for _, line := range outputLines {
for name := range dissolved {
if strings.Contains(line, name) {
delete(dissolved, name)
}
}
if len(dissolved) == 0 {
break
}
}
return dissolved
} }

View file

@ -1,6 +1,7 @@
package optimizer package optimizer
import ( import (
"strings"
"testing" "testing"
) )
@ -232,7 +233,7 @@ func TestOptimizeIntegration(t *testing.T) {
) )
cfg := &Config{EnableLoad: true} cfg := &Config{EnableLoad: true}
output := Optimize(input, cfg) output, _ := Optimize(input, cfg)
// 2 source comments + 3 asm lines (lda b removed) = 5 // 2 source comments + 3 asm lines (lda b removed) = 5
if len(output) != 5 { if len(output) != 5 {
@ -249,7 +250,7 @@ func TestOptimizeWithDebug(t *testing.T) {
) )
cfg := &Config{EnableLoad: true, Debug: true} cfg := &Config{EnableLoad: true, Debug: true}
output := Optimize(input, cfg) output, _ := Optimize(input, cfg)
// Header + 2 kept lines + 1 removed annotation = 4 // Header + 2 kept lines + 1 removed annotation = 4
if len(output) != 4 { if len(output) != 4 {
@ -516,3 +517,218 @@ func TestPassStoreReloadIO(t *testing.T) {
} }
}) })
} }
func TestPassRegDead_DeadStore(t *testing.T) {
input := []string{
"\tlda #$05",
"\tsta myFunc_temp",
"\tsta $d020",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"myFunc_temp": true,
},
}
output, dissolved := Optimize(input, cfg)
if !dissolved["myFunc_temp"] {
t.Error("expected myFunc_temp to be dissolved")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "myFunc_temp") {
t.Errorf("expected sta myFunc_temp to be removed, got:\n%s", joined)
}
}
func TestPassRegDead_KeptStoreWithLoad(t *testing.T) {
input := []string{
"\tlda #$05",
"\tsta myFunc_temp",
"\tlda myFunc_temp",
"\tsta $d020",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"myFunc_temp": true,
},
}
output, dissolved := Optimize(input, cfg)
if dissolved["myFunc_temp"] {
t.Error("expected myFunc_temp NOT to be dissolved")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "myFunc_temp") {
t.Errorf("expected sta myFunc_temp to be kept, got:\n%s", joined)
}
}
func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
input := []string{
"\tlda #$05",
"\tsta myFunc_temp",
"myskip:",
"\tnop",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"myFunc_temp": true,
},
}
output, _ := Optimize(input, cfg)
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta myFunc_temp") {
t.Errorf("expected sta myFunc_temp to be kept (label), got:\n%s", joined)
}
}
func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) {
input := []string{
"\tlda #$05",
"\tsta normalVar",
"\tsta $d020",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{},
}
output, _ := Optimize(input, cfg)
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta normalVar") {
t.Errorf("expected sta normalVar to be kept (not a register var), got:\n%s", joined)
}
}
func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) {
input := []string{
"\tlda #$05",
"\tsta myFunc_temp",
"\tsta $d020",
"\trts",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"myFunc_temp": true,
},
}
output, dissolved := Optimize(input, cfg)
if !dissolved["myFunc_temp"] {
t.Error("expected myFunc_temp to be dissolved")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "sta myFunc_temp") {
t.Errorf("expected sta myFunc_temp to be removed (dead before rts), got:\n%s", joined)
}
}
func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
// sta regVar; lda other → A clobbered, but regVar is reloaded later → keep store
input := []string{
"\tlda 53281",
"\tsta spill_me_bg",
"\tlda 53282",
"\tsta spill_me_mc1",
"\tlda spill_me_bg",
"\tsta 53282",
"\trts",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"spill_me_bg": true,
"spill_me_mc1": true,
},
}
output, dissolved := Optimize(input, cfg)
if dissolved["spill_me_bg"] {
t.Error("spill_me_bg should NOT be dissolved — its value is reloaded")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta spill_me_bg") {
t.Errorf("expected sta spill_me_bg to be kept (reloaded after A clobber), got:\n%s", joined)
}
}
func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) {
// sta → and (A clobbered) → POKE (uses A directly) → rts. Store dead.
input := []string{
"\tlda 56576",
"\tsta dissolve_me_temp",
"\tand #$fc",
"\tsta dissolve_me_temp",
"\tsta 56576",
"\trts",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"dissolve_me_temp": true,
},
}
output, dissolved := Optimize(input, cfg)
if !dissolved["dissolve_me_temp"] {
t.Error("dissolve_me_temp should be dissolved — never reloaded after either store")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "sta dissolve_me_temp") {
t.Errorf("expected all sta dissolve_me_temp to be removed, got:\n%s", joined)
}
}
func TestPassRegDead_CallSiteKeepsStore(t *testing.T) {
// sta regVar; jsr foo; ... no lda regVar ...; rts → store kept (conservative)
// The callee may modify A, so the store survives even though nothing
// reloads regVar after the call.
input := []string{
"\tlda 53281",
"\tsta call_val",
"\tjsr helper",
"\trts",
}
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"call_val": true,
},
}
output, dissolved := Optimize(input, cfg)
if dissolved["call_val"] {
t.Error("call_val should NOT be dissolved — jsr is conservative")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta call_val") {
t.Errorf("expected sta call_val to be kept (before jsr), got:\n%s", joined)
}
}

View file

@ -0,0 +1,71 @@
package optimizer
// passRegDead eliminates dead stores to REGISTER variables.
// A store is dead if no matching load (lda regVar) follows before A is
// clobbered or a label is reached. This is safe because REGISTER variables
// have a contract: nothing external can observe their memory location.
func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
var result []asmLine
for i := 0; i < len(lines); i++ {
line := lines[i]
if line.isCode && line.opcode == "sta" && line.operand != "" && registerVars[line.operand] {
if isRegStoreDead(lines, i+1, line.operand) {
continue
}
}
result = append(result, line)
}
return result
}
// isRegStoreDead scans forward from start to determine whether a store to a
// REGISTER variable operand is dead. Returns true if the store can be removed.
// A store is dead only if no matching lda is found anywhere forward before
// a label or control-flow-ending instruction.
func isRegStoreDead(lines []asmLine, start int, operand string) bool {
for i := start; i < len(lines); i++ {
l := lines[i]
if l.optMarker || l.isComment {
continue
}
if l.isLabel {
return false
}
if l.isCode && isCallSite(l) {
return false // callee may modify A; conservatively keep the store
}
if l.isCode && blocksFlow(l) {
return true // rts/jmp/brk/rti — execution ends here
}
if l.isCode && l.opcode == "lda" && l.operand == operand {
return false // the value IS read from memory later
}
}
return true // end of block reached with no matching load
}
// blocksFlow returns true for instructions that unconditionally end the current
// execution path: rts, jmp, brk, rti
func blocksFlow(line asmLine) bool {
switch line.opcode {
case "rts", "jmp", "brk", "rti":
return true
}
return false
}
// isCallSite returns true for instructions that transfer control to a callee
// that may modify A, X, Y. The store before a call is conservatively kept.
func isCallSite(line asmLine) bool {
return line.opcode == "jsr"
}

19
main.go
View file

@ -71,6 +71,10 @@ func main() {
// Default mode: treat as build command with implicit arguments // Default mode: treat as build command with implicit arguments
// Parse arguments flexibly // Parse arguments flexibly
var inputFile, outputFile string var inputFile, outputFile string
opt := false
optDebug := false
optC64 := false
var optExcludes []string
args := os.Args[1:] args := os.Args[1:]
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {
@ -94,6 +98,17 @@ func main() {
printUsage() printUsage()
os.Exit(1) os.Exit(1)
} }
} else if arg == "--opt" || arg == "-O" {
opt = true
} else if arg == "--opt-debug" {
optDebug = true
} else if arg == "--opt-exclude-c64-io" {
optC64 = true
} else if arg == "--opt-exclude" {
if i+1 < len(args) {
optExcludes = append(optExcludes, args[i+1])
i++ // Skip next arg
}
} else if !strings.HasPrefix(arg, "-") && inputFile == "" { } else if !strings.HasPrefix(arg, "-") && inputFile == "" {
// First non-flag argument is the input file // First non-flag argument is the input file
inputFile = arg inputFile = arg
@ -115,13 +130,13 @@ func main() {
// Determine mode by output extension // Determine mode by output extension
if strings.HasSuffix(strings.ToLower(outputFile), ".prg") { if strings.HasSuffix(strings.ToLower(outputFile), ".prg") {
// Build mode (compile + assemble) // Build mode (compile + assemble)
if err := build(inputFile, outputFile, false, false, false, false, false, nil); err != nil { if err := build(inputFile, outputFile, false, false, opt, optDebug, optC64, optExcludes); err != nil {
handleError(err) handleError(err)
} }
fmt.Println("Build successful.") fmt.Println("Build successful.")
} else { } else {
// Compile mode (assembly only) // Compile mode (assembly only)
if err := compileOnly(inputFile, outputFile, false, false, false, nil); err != nil { if err := compileOnly(inputFile, outputFile, opt, optDebug, optC64, optExcludes); err != nil {
handleError(err) handleError(err)
} }
fmt.Println("Compilation successful.") fmt.Println("Compilation successful.")