diff --git a/examples/build_all.sh b/examples/build_all.sh index 4000d68..4681182 100755 --- a/examples/build_all.sh +++ b/examples/build_all.sh @@ -5,7 +5,7 @@ for dir in */; do file="$dir/$name.c65" if [ -f "$file" ]; then echo "=== Building $name ===" - c65gm "$file" + c65gm build --opt -i "$file" echo "" fi done diff --git a/examples/for_byte_max_test/cm.sh b/examples/for_byte_max_test/cm.sh index e1101cf..050d166 100755 --- a/examples/for_byte_max_test/cm.sh +++ b/examples/for_byte_max_test/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="for_byte_max_test" # Compile and assemble directly -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/hires/cm.sh b/examples/hires/cm.sh index de9eb50..f017acc 100755 --- a/examples/hires/cm.sh +++ b/examples/hires/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="hires" # Compile and assemble directly -c65gm ${PROGNAME}.c65 \ No newline at end of file +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 \ No newline at end of file diff --git a/examples/hires/hires.c65 b/examples/hires/hires.c65 index f3a376e..beb2a3a 100644 --- a/examples/hires/hires.c65 +++ b/examples/hires/hires.c65 @@ -5,7 +5,7 @@ GOTO start FUNC sethires - BYTE b + BYTE REGISTER b b = PEEK $d011 b = b | 32 //enable bitmap mode POKE $d011, b diff --git a/examples/irq_demo/cm.sh b/examples/irq_demo/cm.sh index 62064bd..091d1db 100755 --- a/examples/irq_demo/cm.sh +++ b/examples/irq_demo/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="irq_demo" # Compile and assemble directly -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/load_binary_demo/cm.sh b/examples/load_binary_demo/cm.sh index c0fc561..7614ef9 100755 --- a/examples/load_binary_demo/cm.sh +++ b/examples/load_binary_demo/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="load_binary_demo" # Compile and assemble directly -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/memlib_demo/cm.sh b/examples/memlib_demo/cm.sh index da08fdd..2f3ba40 100755 --- a/examples/memlib_demo/cm.sh +++ b/examples/memlib_demo/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="memlib_demo" # Compile and assemble directly -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/memlib_demo2/cm.sh b/examples/memlib_demo2/cm.sh index 6335c8d..73dad53 100644 --- a/examples/memlib_demo2/cm.sh +++ b/examples/memlib_demo2/cm.sh @@ -1,3 +1,3 @@ #!/bin/sh PROGNAME="memlib_demo2" -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/multdiv_demo/cm.sh b/examples/multdiv_demo/cm.sh index 894533b..7e25c83 100755 --- a/examples/multdiv_demo/cm.sh +++ b/examples/multdiv_demo/cm.sh @@ -1,3 +1,3 @@ #!/bin/sh PROGNAME="multdiv_demo" -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/multicolorbm/multicolorbm.c65 b/examples/multicolorbm/multicolorbm.c65 index 2563620..1cd3ddc 100644 --- a/examples/multicolorbm/multicolorbm.c65 +++ b/examples/multicolorbm/multicolorbm.c65 @@ -5,7 +5,7 @@ GOTO start FUNC setmulti - BYTE b + BYTE REGISTER b b = PEEK $d011 b = b | 32 POKE $d011, b diff --git a/examples/multicolorbm_v2/multicolorbm_v2.c65 b/examples/multicolorbm_v2/multicolorbm_v2.c65 index 35a417e..04966e1 100644 --- a/examples/multicolorbm_v2/multicolorbm_v2.c65 +++ b/examples/multicolorbm_v2/multicolorbm_v2.c65 @@ -90,7 +90,7 @@ ENDSCRIPT // VIC-II multi-color bitmap setup (unchanged from v1) //----------------------------------------------------------- FUNC setmulti - BYTE b + BYTE REGISTER b b = PEEK $d011 b = b | 32 POKE $d011, b diff --git a/examples/script_library_demo/cm.sh b/examples/script_library_demo/cm.sh index 2585626..880fb6d 100755 --- a/examples/script_library_demo/cm.sh +++ b/examples/script_library_demo/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="script_library_demo" # Compile and assemble directly -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/shift_demo/cm.sh b/examples/shift_demo/cm.sh index a670090..4e5f824 100755 --- a/examples/shift_demo/cm.sh +++ b/examples/shift_demo/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="shift_demo" # Compile and assemble directly -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/shift_demo/shift_demo.c65 b/examples/shift_demo/shift_demo.c65 index afa1bc8..e4bf02d 100644 --- a/examples/shift_demo/shift_demo.c65 +++ b/examples/shift_demo/shift_demo.c65 @@ -60,8 +60,8 @@ FEND //----------------------------------------------------------- FUNC print_binary({BYTE val}) BYTE i = 7 - BYTE mask - BYTE bit + BYTE REGISTER mask + BYTE REGISTER bit lib_cbmio_print(" %") @@ -139,8 +139,8 @@ FUNC demo_bit_manipulation // Extract color components from C64 color byte // C64 color: bits 7-4 = background, bits 3-0 = foreground BYTE color = $3E // Background: 3, Foreground: E - BYTE background - BYTE foreground + BYTE REGISTER background + BYTE REGISTER foreground background = color >> 4 foreground = color & $0F @@ -159,7 +159,7 @@ FUNC demo_bit_manipulation // Create bit masks lib_cbmio_printlf("bit masks:") - BYTE mask + BYTE REGISTER mask mask = 1 << 0 lib_cbmio_print("1 << 0 = $") @@ -287,7 +287,7 @@ FUNC demo_word_operations lib_cbmio_printlf("") // Byte to word conversion with shift - BYTE small = $81 + BYTE REGISTER small = $81 WORD large large = small << 2 // Zero-extends byte to word, then shifts @@ -323,8 +323,8 @@ FUNC demo_c64_example lib_cbmio_printlf("") // Fire button (bit 4) - BYTE fire_mask - BYTE fire_check + BYTE REGISTER fire_mask + BYTE REGISTER fire_check fire_mask = 1 << 4 fire_check = joystick & fire_mask IF fire_check = 0 @@ -334,8 +334,8 @@ FUNC demo_c64_example ENDIF // Up button (bit 0) - BYTE up_mask - BYTE up_check + BYTE REGISTER up_mask + BYTE REGISTER up_check up_mask = 1 << 0 up_check = joystick & up_mask IF up_check = 0 @@ -345,8 +345,8 @@ FUNC demo_c64_example ENDIF // Right button (bit 3) - BYTE right_mask - BYTE right_check + BYTE REGISTER right_mask + BYTE REGISTER right_check right_mask = 1 << 3 right_check = joystick & right_mask IF right_check = 0 @@ -357,7 +357,7 @@ FUNC demo_c64_example lib_cbmio_printlf("") // Extract direction bits to nibble - BYTE direction + BYTE REGISTER direction direction = joystick & $0F // Mask off fire button lib_cbmio_print("direction bits: $") diff --git a/examples/switch_demo/cm.sh b/examples/switch_demo/cm.sh index 8cdf8cf..d779c21 100755 --- a/examples/switch_demo/cm.sh +++ b/examples/switch_demo/cm.sh @@ -2,4 +2,4 @@ # Define filename as variable PROGNAME="switch_demo" # Compile and assemble directly -c65gm ${PROGNAME}.c65 +c65gm build --opt --keep-asm -i ${PROGNAME}.c65 diff --git a/examples/switch_demo/switch_demo.c65 b/examples/switch_demo/switch_demo.c65 index 2a4042d..b057b70 100644 --- a/examples/switch_demo/switch_demo.c65 +++ b/examples/switch_demo/switch_demo.c65 @@ -69,8 +69,8 @@ FEND // Test 3: Nested SWITCH statements //----------------------------------------------------------- FUNC test_nested_switch - BYTE outer - BYTE inner + BYTE REGISTER outer + BYTE REGISTER inner LET outer = 2 LET inner = 3 @@ -180,8 +180,8 @@ FEND // Test 7: SWITCH with variable cases (not just literals) //----------------------------------------------------------- FUNC test_variables - BYTE match_val1 - BYTE match_val2 + BYTE REGISTER match_val1 + BYTE REGISTER match_val2 WORD match_val3 LET match_val1 = 15 diff --git a/internal/commands/byte.go b/internal/commands/byte.go index 02c6bb6..c08391c 100644 --- a/internal/commands/byte.go +++ b/internal/commands/byte.go @@ -16,6 +16,8 @@ import ( // BYTE varname = value # byte with init value // BYTE varname @ address # byte at absolute address // 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 { varName string 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 { - // Clear state c.varName = "" c.value = 0 c.isConst = false @@ -45,21 +46,32 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext paramCount := len(params) - // Validate parameter count - if paramCount != 2 && paramCount != 4 && paramCount != 5 { - return fmt.Errorf("BYTE: wrong number of parameters (%d)", paramCount) + // 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 { + return fmt.Errorf("BYTE: wrong number of parameters (%d)", paramCount) + } } var varName string var value int64 scope := ctx.FunctionHandler.CurrentFunction() - // Create constant lookup function constLookup := ctx.SymbolTable.ConstantLookupFunc(ctx.CurrentScope()) switch paramCount { case 2: - // BYTE varname varName = params[1] value = 0 @@ -67,10 +79,16 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext 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) + } case 4: - // BYTE varname = value OR BYTE varname @ address varName = params[1] operator := params[2] valueStr := params[3] @@ -85,14 +103,22 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext } if operator == "=" { - // BYTE varname = value if value < 0 || value > 255 { return fmt.Errorf("BYTE: init value %d out of range (0-255)", value) } - err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line) + 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) + } } 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 { 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: - // BYTE CONST varname = value + if register { + return fmt.Errorf("BYTE REGISTER: unexpected additional parameters") + } constKeyword := strings.ToUpper(params[1]) varName = params[2] operator := params[3] diff --git a/internal/commands/byte_test.go b/internal/commands/byte_test.go index 8bbe9ce..191941b 100644 --- a/internal/commands/byte_test.go +++ b/internal/commands/byte_test.go @@ -438,3 +438,66 @@ func TestByteCommand_ConstantNotFound(t *testing.T) { 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") + } +} diff --git a/internal/commands/word.go b/internal/commands/word.go index 2ada2d7..c7a4704 100644 --- a/internal/commands/word.go +++ b/internal/commands/word.go @@ -47,6 +47,11 @@ func (c *WordCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext 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 if paramCount != 2 && paramCount != 4 && paramCount != 5 { return fmt.Errorf("WORD: wrong number of parameters (%d)", paramCount) diff --git a/internal/commands/word_test.go b/internal/commands/word_test.go index 9225bf0..60e6fcb 100644 --- a/internal/commands/word_test.go +++ b/internal/commands/word_test.go @@ -636,3 +636,25 @@ func TestWordCommand_MultipleStrings(t *testing.T) { 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) + } +} diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index cf2e0d9..4b08ac7 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -15,6 +15,7 @@ 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 @@ -197,6 +198,11 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { } 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()) codePart = codePart[:start] + expandedName + codePart[end+1:] // Continue searching after the replacement @@ -271,7 +277,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { // Peephole optimization pass 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 @@ -281,7 +289,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma() // Check for unused variables and print warnings (skip variables in functions with remove pragma) - warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma) + warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma, c.dissolvedVars) for _, warning := range warnings { _, _ = fmt.Fprintf(os.Stderr, "%s\n", warning) } @@ -342,11 +350,20 @@ func (c *Compiler) getOptimizerConfig() *optimizer.Config { cfg.EnableJmp = true cfg.EnableSelf = true cfg.EnableStoreLoad = true + cfg.EnableRegisterVars = true } if c.CmdlineDebug { 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() { return nil } @@ -724,7 +741,7 @@ func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bo output = append(output, "") // 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...) } diff --git a/internal/compiler/funchandler.go b/internal/compiler/funchandler.go index 013d6d0..76695d9 100644 --- a/internal/compiler/funchandler.go +++ b/internal/compiler/funchandler.go @@ -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) } + 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{ Symbol: sym, Direction: direction, @@ -614,15 +619,26 @@ func (fh *FunctionHandler) processConstValue(value uint16, param *FuncParam, fun 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 { parts := strings.Fields(decl) - if len(parts) != 2 && len(parts) != 4 { - return fmt.Errorf("implicit declaration must be 'TYPE name' or 'TYPE name @ addr', got: %q", decl) + if len(parts) < 2 || len(parts) > 4 { + 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]) - 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 switch typeStr { @@ -634,33 +650,48 @@ func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line return fmt.Errorf("implicit declaration type must be BYTE or WORD, got: %s", typeStr) } - if len(parts) == 2 { - // Simple: BYTE name or WORD name + if register && kind != KindByte { + 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) } - // Extended: BYTE name @ address or WORD name @ address - operator := parts[2] - addrStr := parts[3] + // Extended: TYPE [REGISTER] name @ address + if len(parts) == 4+typeIdx { + operator := parts[2+typeIdx] + addrStr := parts[3+typeIdx] - if operator != "@" { - return fmt.Errorf("expected '@' operator, got: %q", operator) + if register { + return fmt.Errorf("REGISTER variable cannot be @-mapped; REGISTER is incompatible with fixed address") + } + + if operator != "@" { + return fmt.Errorf("expected '@' operator, got: %q", operator) + } + + constLookup := fh.symTable.ConstantLookupFunc([]string{funcName}) + + addr, err := utils.EvaluateExpression(addrStr, constLookup) + if err != nil { + return fmt.Errorf("invalid address %q: %w", addrStr, err) + } + + if addr < 0 || addr > 0xFFFF { + return fmt.Errorf("absolute address $%X out of range", addr) + } + + return fh.symTable.AddAbsolute(varName, funcName, kind, uint16(addr), line) } - // Create constant lookup function for address evaluation - constLookup := fh.symTable.ConstantLookupFunc([]string{funcName}) - - // Parse address (supports $hex and decimal) using EvaluateExpression - addr, err := utils.EvaluateExpression(addrStr, constLookup) - if err != nil { - return fmt.Errorf("invalid address %q: %w", addrStr, err) - } - - if addr < 0 || addr > 0xFFFF { - return fmt.Errorf("absolute address $%X out of range", addr) - } - - 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) @@ -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, "}") { isImplicit = true implicitDecl = varName[1 : len(varName)-1] // strip { } @@ -873,7 +904,12 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) { if len(parts) < 2 { return 0, "", false, "", fmt.Errorf("invalid implicit declaration: %q", varName) } - varName = parts[1] + // 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] + } } return direction, varName, isImplicit, implicitDecl, nil diff --git a/internal/compiler/funchandler_test.go b/internal/compiler/funchandler_test.go index 5d47716..af2babe 100644 --- a/internal/compiler/funchandler_test.go +++ b/internal/compiler/funchandler_test.go @@ -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") + } +} diff --git a/internal/compiler/scriptexec.go b/internal/compiler/scriptexec.go index b83aa96..076e8bc 100644 --- a/internal/compiler/scriptexec.go +++ b/internal/compiler/scriptexec.go @@ -214,7 +214,10 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b scriptText := strings.Join(texts, "\n") // 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 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 -func expandVariables(text string, ctx *CompilerContext) string { +func expandVariables(text string, ctx *CompilerContext) (string, error) { result := text for { start := strings.IndexByte(result, '|') @@ -312,10 +315,14 @@ func expandVariables(text string, ctx *CompilerContext) string { end += start + 1 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()) 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. @@ -547,7 +554,10 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaS // Expand |varname| -> actual variable names in the OUTPUT // 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 } diff --git a/internal/compiler/symboltable.go b/internal/compiler/symboltable.go index e4a7949..2b24be7 100644 --- a/internal/compiler/symboltable.go +++ b/internal/compiler/symboltable.go @@ -26,6 +26,7 @@ const ( FlagAbsolute FlagZeroPage FlagLabelRef + FlagRegister ) // 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) IsAbsolute() bool { return s.Has(FlagAbsolute) } 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) } // 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) func (st *SymbolTable) AddConst(name, scope string, kind VarKind, value uint16, line preproc.Line) error { var flags SymbolFlags @@ -340,15 +356,13 @@ func (st *SymbolTable) ConstantLookupFunc(currentScopes []string) func(string) ( // CheckUnused returns warnings for unused variables // 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) -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 for _, sym := range st.symbols { // Skip constants and absolute variables (they shouldn't track usage) 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() { - // This would be an internal error, but we'll just skip it continue } continue @@ -359,6 +373,11 @@ func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string { 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 if st.pragma != nil { 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 if !sym.IsUsed() { - // Format warning message with file and line info var scopeInfo string if 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) -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 hasVars := false @@ -515,6 +533,10 @@ func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool) []string if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] { continue } + // Skip dissolved REGISTER variables (optimizer eliminated all references) + if sym.IsRegister() && dissolvedVars != nil && dissolvedVars[sym.FullName()] { + continue + } hasVars = true var line string diff --git a/internal/compiler/symboltable_test.go b/internal/compiler/symboltable_test.go index 16cdc7a..0913128 100644 --- a/internal/compiler/symboltable_test.go +++ b/internal/compiler/symboltable_test.go @@ -580,7 +580,7 @@ func TestGenerateVariables(t *testing.T) { // Absolute (should be skipped) 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 { t.Fatal("expected output lines") @@ -630,7 +630,7 @@ func TestGenerateEmpty(t *testing.T) { if lines := GenerateAbsolutes(st, nil); lines != nil { 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") } @@ -652,7 +652,7 @@ func TestGenerateScopedVariables(t *testing.T) { 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}) - lines := GenerateVariables(st, nil) + lines := GenerateVariables(st, nil, nil) output := strings.Join(lines, "\n") // Check full names are used @@ -676,7 +676,7 @@ func TestGenerateHexLowercase(t *testing.T) { constLines := GenerateConstants(st, nil) absLines := GenerateAbsolutes(st, nil) - varLines := GenerateVariables(st, nil) + varLines := GenerateVariables(st, nil, nil) output := strings.Join(append(append(constLines, absLines...), varLines...), "\n") @@ -716,7 +716,7 @@ func TestUsageTracking(t *testing.T) { } // Check that warning is generated - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 1 { 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 - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 0 { 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 - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 0 { 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 - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 0 { 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"}) // Check warnings - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 2 { 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 - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 0 { 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 - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 1 { t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings)) } @@ -922,7 +922,7 @@ func TestUsageTracking(t *testing.T) { st.Lookup("used2", []string{"func1"}) // Check warnings - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 2 { 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 - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 1 { 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{}) // Now no warning should be generated - warnings = st.CheckUnused(nil) + warnings = st.CheckUnused(nil, nil) if len(warnings) != 0 { 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 - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 1 { t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings)) } @@ -1067,9 +1067,101 @@ func TestUsageTracking(t *testing.T) { } // No warnings should be generated - warnings := st.CheckUnused(nil) + warnings := st.CheckUnused(nil, nil) if len(warnings) != 0 { 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)) + } +} diff --git a/internal/optimizer/config.go b/internal/optimizer/config.go index 7dec2f2..4335cce 100644 --- a/internal/optimizer/config.go +++ b/internal/optimizer/config.go @@ -9,32 +9,35 @@ import ( ) type Config struct { - EnableLoad bool - EnableImm bool - EnableJmp bool - EnableSelf bool - EnableStoreLoad bool - Debug bool - ShowMarkers bool - IOMap [65536]bool + EnableLoad bool + EnableImm bool + EnableJmp bool + EnableSelf bool + EnableStoreLoad bool + EnableRegisterVars bool + Debug bool + ShowMarkers bool + IOMap [65536]bool + RegisterVars map[string]bool } func NewConfig(ps preproc.PragmaSet) *Config { all := ps.GetPragma("_P_OPT_ALL") != "" && ps.GetPragma("_P_OPT_ALL") != "0" return &Config{ - EnableLoad: all || (ps.GetPragma("_P_OPT_LOAD") != "" && ps.GetPragma("_P_OPT_LOAD") != "0"), - EnableImm: all || (ps.GetPragma("_P_OPT_IMM") != "" && ps.GetPragma("_P_OPT_IMM") != "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"), - EnableStoreLoad: all || (ps.GetPragma("_P_OPT_STLD") != "" && ps.GetPragma("_P_OPT_STLD") != "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"), + EnableLoad: all || (ps.GetPragma("_P_OPT_LOAD") != "" && ps.GetPragma("_P_OPT_LOAD") != "0"), + EnableImm: all || (ps.GetPragma("_P_OPT_IMM") != "" && ps.GetPragma("_P_OPT_IMM") != "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"), + 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"), + ShowMarkers: (ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"), } } 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. diff --git a/internal/optimizer/optimizer.go b/internal/optimizer/optimizer.go index 935316d..7fe04dc 100644 --- a/internal/optimizer/optimizer.go +++ b/internal/optimizer/optimizer.go @@ -1,11 +1,17 @@ package optimizer +import "strings" + // Optimize applies all enabled peephole passes to the generated ASM lines. // Each pass runs sequentially on a parsed representation of the lines. // @@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() { - return lines + return lines, dissolved } parsed := parseLines(lines) @@ -26,11 +32,41 @@ func Optimize(lines []string, cfg *Config) []string { if cfg.EnableSelf { parsed = passSelfAssignment(parsed) } + if cfg.EnableRegisterVars && len(cfg.RegisterVars) > 0 { + parsed = passRegDead(parsed, cfg.RegisterVars) + } parsed = stripOptMarkers(parsed) if cfg.Debug { original = stripOptMarkers(original) 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 } diff --git a/internal/optimizer/optimizer_test.go b/internal/optimizer/optimizer_test.go index 2a621c0..01cc7c8 100644 --- a/internal/optimizer/optimizer_test.go +++ b/internal/optimizer/optimizer_test.go @@ -1,6 +1,7 @@ package optimizer import ( + "strings" "testing" ) @@ -232,7 +233,7 @@ func TestOptimizeIntegration(t *testing.T) { ) cfg := &Config{EnableLoad: true} - output := Optimize(input, cfg) + output, _ := Optimize(input, cfg) // 2 source comments + 3 asm lines (lda b removed) = 5 if len(output) != 5 { @@ -249,7 +250,7 @@ func TestOptimizeWithDebug(t *testing.T) { ) cfg := &Config{EnableLoad: true, Debug: true} - output := Optimize(input, cfg) + output, _ := Optimize(input, cfg) // Header + 2 kept lines + 1 removed annotation = 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) + } +} diff --git a/internal/optimizer/pass_regdead.go b/internal/optimizer/pass_regdead.go new file mode 100644 index 0000000..90492a2 --- /dev/null +++ b/internal/optimizer/pass_regdead.go @@ -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" +} diff --git a/main.go b/main.go index 140b4ad..a9503d5 100644 --- a/main.go +++ b/main.go @@ -71,11 +71,15 @@ func main() { // Default mode: treat as build command with implicit arguments // Parse arguments flexibly var inputFile, outputFile string + opt := false + optDebug := false + optC64 := false + var optExcludes []string args := os.Args[1:] - + for i := 0; i < len(args); i++ { arg := args[i] - + if arg == "-i" || arg == "-in" { if i+1 < len(args) { inputFile = args[i+1] @@ -94,34 +98,45 @@ func main() { printUsage() 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 == "" { // First non-flag argument is the input file inputFile = arg } } - + if inputFile == "" { fmt.Fprintln(os.Stderr, "Error: No input file specified") printUsage() os.Exit(1) } - + // Default output based on input if outputFile == "" { base := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile)) outputFile = base + ".prg" // Default to build mode (.prg) } - + // Determine mode by output extension if strings.HasSuffix(strings.ToLower(outputFile), ".prg") { // 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) } fmt.Println("Build successful.") } else { // 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) } fmt.Println("Compilation successful.")