Compare commits

..

No commits in common. "main" and "peephole_optimizer" have entirely different histories.

73 changed files with 506 additions and 6136 deletions

View file

@ -1 +1 @@
6 5

View file

@ -1,5 +1,5 @@
#FROM ghcr.io/anomalyco/opencode:1.14.48 FROM ghcr.io/anomalyco/opencode:1.14.48
FROM ghcr.io/anomalyco/opencode:latest #FROM ghcr.io/anomalyco/opencode:latest
RUN apk add --no-cache go gcc musl-dev RUN apk add --no-cache go gcc musl-dev

View file

@ -39,7 +39,7 @@ c65gm compiles high-level source code into ACME assembler syntax for the 6502 pr
- **Functions**: Named functions with parameters and call graph analysis - **Functions**: Named functions with parameters and call graph analysis
- **Control flow**: IF/ENDIF, WHILE/WEND, FOR loops, SWITCH/CASE - **Control flow**: IF/ENDIF, WHILE/WEND, FOR loops, SWITCH/CASE
- **Memory operations**: PEEK/POKE/PEEKW/POKEW with zero-page optimization. Access registers as variables. - **Memory operations**: PEEK/POKE/PEEKW/POKEW with zero-page optimization. Access registers as variables.
- **Operators**: Arithmetic (ADD, SUB), bitwise (AND, OR, XOR), shifts (SHL, SHR) - **Operators**: Arithmetic (ADD, SUB), bitwise (AND, OR, XOR)
- **Preprocessor**: File inclusion, macros, conditional compilation, Starlark scripting - **Preprocessor**: File inclusion, macros, conditional compilation, Starlark scripting
- **Standard library**: C64 screen/kernal routines, memory management, string handling, graphics (Koala), FAT16 filesystem, and more (accessed via `#include <file>`, path set by `C65LIBPATH` environment variable) - **Standard library**: C64 screen/kernal routines, memory management, string handling, graphics (Koala), FAT16 filesystem, and more (accessed via `#include <file>`, path set by `C65LIBPATH` environment variable)
- **Optimizations**: Constant folding, self-assignment detection - **Optimizations**: Constant folding, self-assignment detection
@ -308,7 +308,7 @@ The optimizer never removes **reads or stores** to addresses marked in the I/O e
2. **CLI shorthand**: `--opt-exclude-c64-io` for the full C64 I/O page 2. **CLI shorthand**: `--opt-exclude-c64-io` for the full C64 I/O page
3. **Pragma**: `#PRAGMA _P_OPT_IO $D000 $DFFF` 3. **Pragma**: `#PRAGMA _P_OPT_IO $D000 $DFFF`
Variable names (like `vic2`, `BORDER_COLOR`) are NOT checked against the I/O map — only literal numeric addresses (hex `$D020` or decimal `53280`) are. For `@`-mapped variables that point to I/O registers, use the `_P_OPT_IO` pragma with their address range. Variable names (like `vic2`, `BORDER_COLOR`) are NOT checked against the I/O map — only literal hex addresses are. For `@`-mapped variables that point to I/O registers, use the `_P_OPT_IO` pragma with their address range.
### Environment Variables ### Environment Variables
- **`C65LIBPATH`**: Search path for `#INCLUDE <file>` directives - **`C65LIBPATH`**: Search path for `#INCLUDE <file>` directives

View file

@ -91,7 +91,6 @@ BREAK
``` ```
// BREAK in FOR loop // BREAK in FOR loop
BYTE i
FOR i = 0 TO 100 FOR i = 0 TO 100
IF i = 50 IF i = 50
BREAK BREAK
@ -113,14 +112,12 @@ WEND
## BYTE ## BYTE
Declares an 8-bit variable, register-hinted temporary, or constant. Declares an 8-bit variable or constant.
**Syntax:** **Syntax:**
``` ```
BYTE <varname> BYTE <varname>
BYTE <varname> = <value> BYTE <varname> = <value>
BYTE REGISTER <varname>
BYTE REGISTER <varname> = <value>
BYTE <varname> @ <address> BYTE <varname> @ <address>
BYTE CONST <varname> = <value> BYTE CONST <varname> = <value>
``` ```
@ -129,17 +126,10 @@ BYTE CONST <varname> = <value>
``` ```
BYTE counter BYTE counter
BYTE speed = 5 BYTE speed = 5
BYTE REGISTER temp
BYTE REGISTER scratch = 0
BYTE screen @ $D020 BYTE screen @ $D020
BYTE CONST MAX_SPEED = 10 BYTE CONST MAX_SPEED = 10
``` ```
`REGISTER` is a storage hint for the optimizer. The variable may be kept in a
CPU register (A/X/Y) and its memory allocation can be eliminated entirely.
Only valid inside `FUNC`/`FEND` blocks; incompatible with `@` and `CONST`.
See the REGISTER section in `language.md` for details.
--- ---
## CALL ## CALL
@ -252,8 +242,7 @@ See [FUNC](#func) for syntax and examples.
## FOR ## FOR
Loop with automatic counter increment. The iterator variable must be declared Loop with automatic counter increment.
beforehand.
**Syntax:** **Syntax:**
``` ```
@ -264,7 +253,6 @@ FOR <iterator> = <start_value> TO <end_value>
``` ```
// FOR loop with literal values // FOR loop with literal values
BYTE i
FOR i = 0 TO 10 FOR i = 0 TO 10
screen = i screen = i
NEXT NEXT
@ -272,7 +260,6 @@ NEXT
``` ```
// FOR loop with variables // FOR loop with variables
BYTE counter
FOR counter = start TO finish FOR counter = start TO finish
process(counter) process(counter)
NEXT NEXT
@ -627,15 +614,15 @@ For operating with offsets the address parameter must be an absolute WORD variab
**Syntax:** **Syntax:**
``` ```
POKE <address>[<offset>], <value> POKE <address>[<offset>] WITH <value>
``` ```
**Examples:** **Examples:**
``` ```
POKE $D020, 0 POKE $D020 WITH 0
POKE screenPtr[index], char POKE screenPtr[index] WITH char
POKE buffer[5], data POKE buffer[5] WITH data
POKE pointer, value POKE pointer WITH value
``` ```
--- ---
@ -649,14 +636,14 @@ For operating with offsets the address parameter must be an absolute WORD variab
**Syntax:** **Syntax:**
``` ```
POKEW <address>[<offset>], <value> POKEW <address>[<offset>] WITH <value>
``` ```
**Examples:** **Examples:**
``` ```
POKEW $0314, handler POKEW $0314 WITH handler
POKEW dataPtr[0], value POKEW dataPtr[0] WITH value
POKEW buffer[10], address POKEW buffer[10] WITH address
``` ```
--- ---

View file

@ -43,7 +43,6 @@
<item>POINTER</item> <item>POINTER</item>
<item>POKE</item> <item>POKE</item>
<item>POKEW</item> <item>POKEW</item>
<item>REGISTER</item>
<item>SCRIPT</item> <item>SCRIPT</item>
<item>STEP</item> <item>STEP</item>
<item>SUBEND</item> <item>SUBEND</item>

View file

@ -38,7 +38,7 @@ contexts:
scope: meta.preprocessor.c65cm scope: meta.preprocessor.c65cm
keywords: keywords:
- match: '\b(ADD|AND|AS|ASM|BREAK|BYTE|CALL|CASE|CONST|DEC|DECREMENT|DEFAULT|DO|ELSE|ENDASM|ENDIF|ENDSCRIPT|ENDSWITCH|EXIT|FEND|FOR|FUNC|GOSUB|GOTO|IF|INC|INCREMENT|LABEL|LET|LIBRARY|MACRO|NEXT|OR|ORIGIN|PASSING|PEEK|PEEKW|POINTER|POKE|POKEW|REGISTER|SCRIPT|STEP|SUBEND|SUBTRACT|SWITCH|THEN|TO|WHILE|WITH|WEND|WORD|XOR)\b' - match: '\b(ADD|AND|AS|ASM|BREAK|BYTE|CALL|CASE|CONST|DEC|DECREMENT|DEFAULT|DO|ELSE|ENDASM|ENDIF|ENDSCRIPT|ENDSWITCH|EXIT|FEND|FOR|FUNC|GOSUB|GOTO|IF|INC|INCREMENT|LABEL|LET|LIBRARY|MACRO|NEXT|OR|ORIGIN|PASSING|PEEK|PEEKW|POINTER|POKE|POKEW|SCRIPT|STEP|SUBEND|SUBTRACT|SWITCH|THEN|TO|WHILE|WITH|WEND|WORD|XOR)\b'
scope: keyword.control.c65cm scope: keyword.control.c65cm
- match: '\b(in|out|io)\b(?=\s*:)' - match: '\b(in|out|io)\b(?=\s*:)'
scope: storage.modifier.parameter.c65cm scope: storage.modifier.parameter.c65cm

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 build --opt -i "$file" c65gm "$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 build --opt --keep-asm -i ${PROGNAME}.c65 c65gm ${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 build --opt --keep-asm -i ${PROGNAME}.c65 c65gm ${PROGNAME}.c65

View file

@ -5,15 +5,15 @@ GOTO start
FUNC sethires FUNC sethires
BYTE REGISTER b BYTE 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
b = PEEK $d018 b = PEEK $d018
b = b & %11110000 b = b & %11110000
b = b | 8 //enable bitmap mode b = b | 8 //enable bitmap mode
POKE $d018, b POKE $d018 , b
FEND FEND
@ -22,7 +22,7 @@ FEND
FUNC fillmem({WORD start_addr @ $fa} {WORD end_addr @ $fc} {BYTE value}) FUNC fillmem({WORD start_addr @ $fa} {WORD end_addr @ $fc} {BYTE value})
WHILE start_addr <= end_addr WHILE start_addr <= end_addr
POKE start_addr, value POKE start_addr , value
start_addr++ start_addr++
WEND WEND

View file

@ -1,5 +0,0 @@
#!/bin/sh
# Define filename as variable
PROGNAME="irq_demo"
# Compile and assemble directly
c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -1,55 +0,0 @@
//-----------------------------------------------------------
// Simple IRQ Handler Demo
//
// Installs a custom IRQ handler through the kernal vector at
// $0314. The kernal has already saved the registers for us
// before it calls the vector, so our handler does NOT push or
// pull A/X/Y. When we are done we jump into the kernal so it
// can finish the IRQ and RTI properly:
//
// jmp $ea31 - let the kernal do its full IRQ work
// (scan keyboard, blink cursor, update jiffy
// clock, ...) and then RTI
// jmp $ea81 - skip the kernal work, just restore the
// registers the kernal saved and RTI
//
// This handler chains to $ea31 so the machine stays usable.
//-----------------------------------------------------------
#INCLUDE <c64start.c65>
#INCLUDE <c64defs.c65>
GOTO start
WORD CONST IRQ_VECTOR = $0314
WORD handler = @myIRQ // Address of our IRQ handler
FUNC installIRQ
ASM
sei // Disable interrupts while we patch
ENDASM
POKEW IRQ_VECTOR, handler // Point the vector at our handler
ASM
cli // Re-enable interrupts
ENDASM
FEND
LABEL start
installIRQ()
SUBEND //exit back to basic
//-----------------------------------------------------------
// The IRQ handler.
//
// No register saving needed - the kernal did it. Do the work,
// then hand control back to the kernal at $ea31 which restores
// the registers and returns from the interrupt.
//-----------------------------------------------------------
LABEL myIRQ
ASM
inc $0400 // Bump the top-left screen character
jmp $ea31 // Let the kernal finish the IRQ
ENDASM

View file

@ -1 +0,0 @@
x64 -autostartprgmode 1 irq_demo.prg

View file

@ -1,5 +0,0 @@
#!/bin/sh
# Define filename as variable
PROGNAME="load_binary_demo"
# Compile and assemble directly
c65gm build --opt --keep-asm -i ${PROGNAME}.c65

View file

@ -1,50 +0,0 @@
//-----------------------------------------------------------
// load_binary Demo
// Demonstrates loading binary data from a file at compile time
// using the Starlark scripting built-in load_binary().
//
// The SCRIPT block reads screendata.bin (40 bytes of C64
// screen codes) and emits them as assembler data. At runtime,
// a c65gm FOR loop copies the data to screen memory ($0400)
// using WORD pointers with self-modifying code.
//
// Build: c65gm load_binary_demo.c65
//-----------------------------------------------------------
#INCLUDE <c64start.c65>
GOTO start
//-----------------------------------------------------------
// Load binary data at compile time using Starlark load_binary()
// and emit it as data statements in the assembly output.
//-----------------------------------------------------------
SCRIPT
data = load_binary("screendata.bin")
print("; Loaded %d bytes from screendata.bin" % len(data))
print("scrdata:")
for i in range(0, len(data), 8):
row = ", ".join(["%d" % b for b in data[i:i+8]])
print(" !8 " + row)
ENDSCRIPT
LABEL start
// Pointers to source data and screen memory
WORD src
WORD dst
BYTE i
BYTE val
POINTER src TO scrdata
POINTER dst TO $0400
// Copy 40 bytes from scrdata to screen memory
FOR i = 0 TO 39
val = PEEK src
POKE dst, val
src++
dst++
NEXT
SUBEND

View file

@ -1,2 +0,0 @@

 !"#$%&'(

View file

@ -1 +0,0 @@
x64 -autostartprgmode 1 load_binary_demo.prg

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 build --opt --keep-asm -i ${PROGNAME}.c65 c65gm ${PROGNAME}.c65

View file

@ -40,7 +40,7 @@ FUNC wait_key
WEND WEND
// Reset key buffer // Reset key buffer
POKE $c6, 0 POKE $c6 WITH 0
FEND FEND

View file

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

View file

@ -45,7 +45,7 @@ FUNC wait_key
ENDIF ENDIF
WEND WEND
POKE $c6, 0 POKE $c6 WITH 0
FEND FEND
//----------------------------------------------------------- //-----------------------------------------------------------

View file

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

View file

@ -36,7 +36,7 @@ FUNC wait_key
ENDIF ENDIF
WEND WEND
POKE $c6, 0 POKE $c6 WITH 0
FEND FEND
//----------------------------------------------------------- //-----------------------------------------------------------

View file

@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
# Define filename as variable # Define filename as variable
PROGNAME="multicolorbm" PROGNAME="multicolorbm"
# Compile and assemble directly, keep intermediate .asm file, enable optimizations # Compile and assemble directly
c65gm build -i ${PROGNAME}.c65 --keep-asm --opt c65gm ${PROGNAME}.c65

View file

@ -5,19 +5,19 @@ GOTO start
FUNC setmulti FUNC setmulti
BYTE REGISTER b BYTE b
b = PEEK $d011 b = PEEK $d011
b = b | 32 b = b | 32
POKE $d011, b POKE $d011 , b
b = PEEK $d016 b = PEEK $d016
b = b | 16 b = b | 16
POKE $d016, b POKE $d016 , b
b = PEEK $d018 b = PEEK $d018
b = b & %11110000 b = b & %11110000
b = b | 8 b = b | 8
POKE $d018, b POKE $d018 , b
FEND FEND
@ -25,7 +25,7 @@ FEND
FUNC fillmem({WORD start_addr @ $fa} {WORD end_addr @ $fc} {BYTE value}) FUNC fillmem({WORD start_addr @ $fa} {WORD end_addr @ $fc} {BYTE value})
WHILE start_addr <= end_addr WHILE start_addr <= end_addr
POKE start_addr, value POKE start_addr , value
start_addr++ start_addr++
WEND WEND
@ -41,7 +41,7 @@ FUNC main
fillmem(screen, screen+999, $12) fillmem(screen, screen+999, $12)
fillmem(colorram, colorram+999, $03) fillmem(colorram, colorram+999, $03)
POKE $d021, 0 POKE $d021 , 0
WHILE 1 WHILE 1
fillmem($2000, $3fff, %00011011) fillmem($2000, $3fff, %00011011)

View file

@ -1,5 +0,0 @@
#!/bin/sh
# Define filename as variable
PROGNAME="multicolorbm_v2"
# Compile and assemble directly, keep intermediate .asm file, enable optimizations
c65gm build -i ${PROGNAME}.c65 --keep-asm --opt

View file

@ -1,131 +0,0 @@
//-----------------------------------------------------------
// multicolorbm_v2 - Multi-color bitmap demo using SCRIPT MACRO
//
// Uses compile-time code generation via SCRIPT MACRO to produce
// optimized fill routines instead of a generic runtime loop.
// The fill_fast macro analyzes the memory range at compile time
// and selects the most cycle-efficient 6502 fill strategy.
//-----------------------------------------------------------
#INCLUDE <c64start.c65>
#INCLUDE <c64defs.c65>
GOTO start
//-----------------------------------------------------------
// SCRIPT LIBRARY: optimized fill code generation
//
// At compile time, this analyzes the start/end/value and emits
// specialized assembly. Small fills (<256 bytes) use a simple
// X-indexed loop. Whole-page fills use page-based unrolling.
// Large partial-page fills choose the cycle-cheapest strategy.
//-----------------------------------------------------------
SCRIPT LIBRARY
def to_hex(v):
digits = "0123456789abcdef"
return digits[(v >> 12) & 15] + digits[(v >> 8) & 15] + digits[(v >> 4) & 15] + digits[v & 15]
def emit_fill_fast(start, end, value):
total = end - start + 1
print("")
print("; fill_fast $" + to_hex(start) + "..$" + to_hex(end) + " = " + str(total) + " bytes")
print(" lda #" + str(value))
if total < 256:
print(" ldx #" + str(total))
print("-")
print(" sta $" + to_hex(start) + "-1,x")
print(" dex")
print(" bne -")
elif total == 256:
print(" ldx #0")
print("-")
print(" sta $" + to_hex(start) + ",x")
print(" inx")
print(" bne -")
else:
full_pages = int(total / 256)
remain = total - full_pages * 256
cycles_a = (5 * full_pages + 4) * 256 + 9 * remain
cycles_b = (5 * (full_pages + 1) + 4) * 256 - 1
if cycles_a <= cycles_b:
pages = [start + i * 256 for i in range(full_pages)]
print(" ldx #0")
print("-")
for a in pages:
print(" sta $" + to_hex(a) + ",x")
print(" inx")
print(" bne -")
if remain > 0:
print(" ldx #" + str(remain))
print("--")
print(" sta $" + to_hex(start + full_pages * 256) + "-1,x")
print(" dex")
print(" bne --")
else:
pages = [start + i * 256 for i in range(full_pages)]
pages.append(end - 255)
print(" ldx #0")
print("-")
for a in pages:
print(" sta $" + to_hex(a) + ",x")
print(" inx")
print(" bne -")
ENDSCRIPT
//-----------------------------------------------------------
// SCRIPT MACRO: inline fill_fast
//
// Replacements for runtime fillmem(). Generates optimized
// assembly inline at each call site.
//-----------------------------------------------------------
SCRIPT MACRO fill_fast(start, end, value)
emit_fill_fast(start, end, value)
ENDSCRIPT
//-----------------------------------------------------------
// VIC-II multi-color bitmap setup (unchanged from v1)
//-----------------------------------------------------------
FUNC setmulti
BYTE REGISTER b
b = PEEK $d011
b = b | 32
POKE $d011, b
b = PEEK $d016
b = b | 16
POKE $d016, b
b = PEEK $d018
b = b & %11110000
b = b | 8
POKE $d018, b
FEND
//-----------------------------------------------------------
// Main program
//
// Uses @fill_fast macro instead of runtime fillmem() loops.
// Each invocation generates optimal fill code for its range.
//-----------------------------------------------------------
FUNC main
setmulti()
@fill_fast($0400, $0400+999, $12)
@fill_fast(colorram, colorram+999, $03)
POKE $d021, 0
WHILE 1
@fill_fast($2000, $3fff, %00011011)
@fill_fast($2000, $3fff, %01101100)
@fill_fast($2000, $3fff, %10110001)
@fill_fast($2000, $3fff, %11000110)
WEND
FEND
LABEL start
main()

View file

@ -1 +0,0 @@
x64 -autostartprgmode 1 multicolorbm_v2.prg

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 build --opt --keep-asm -i ${PROGNAME}.c65 c65gm ${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 build --opt --keep-asm -i ${PROGNAME}.c65 c65gm ${PROGNAME}.c65

View file

@ -43,7 +43,7 @@ FUNC wait_key
WEND WEND
// Reset key buffer // Reset key buffer
POKE $c6, 0 POKE $c6 WITH 0
FEND FEND
@ -60,8 +60,8 @@ FEND
//----------------------------------------------------------- //-----------------------------------------------------------
FUNC print_binary({BYTE val}) FUNC print_binary({BYTE val})
BYTE i = 7 BYTE i = 7
BYTE REGISTER mask BYTE mask
BYTE REGISTER bit BYTE 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 REGISTER background BYTE background
BYTE REGISTER foreground BYTE 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 REGISTER mask BYTE 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 REGISTER small = $81 BYTE 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 REGISTER fire_mask BYTE fire_mask
BYTE REGISTER fire_check BYTE 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 REGISTER up_mask BYTE up_mask
BYTE REGISTER up_check BYTE 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 REGISTER right_mask BYTE right_mask
BYTE REGISTER right_check BYTE 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 REGISTER direction BYTE 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 build --opt --keep-asm -i ${PROGNAME}.c65 c65gm ${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 REGISTER outer BYTE outer
BYTE REGISTER inner BYTE 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 REGISTER match_val1 BYTE match_val1
BYTE REGISTER match_val2 BYTE match_val2
WORD match_val3 WORD match_val3
LET match_val1 = 15 LET match_val1 = 15

View file

@ -156,26 +156,6 @@ func (c *AndCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext)
} }
} }
// Normalize operands: leverage AND commutativity to swap if needed
// This ensures word operands are in param1 when mixed with bytes
// Makes code generation simpler and more consistent
param1IsByteSized := false
if c.param1IsVar {
param1IsByteSized = (c.param1VarKind == compiler.KindByte)
} else {
param1IsByteSized = ((c.param1Value >> 8) & 0xFF) == 0
}
param2IsWord := c.param2IsVar && c.param2VarKind == compiler.KindWord
if param1IsByteSized && param2IsWord {
// Swap param1 and param2
c.param1VarName, c.param2VarName = c.param2VarName, c.param1VarName
c.param1VarKind, c.param2VarKind = c.param2VarKind, c.param1VarKind
c.param1Value, c.param2Value = c.param2Value, c.param1Value
c.param1IsVar, c.param2IsVar = c.param2IsVar, c.param1IsVar
}
return nil return nil
} }
@ -200,110 +180,51 @@ func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
} }
// At least one param is a variable - generate AND code // At least one param is a variable - generate AND code
// Load param1
// Same variable on both sides: a & a = a if c.param1IsVar {
if c.param1IsVar && c.param2IsVar && c.param1VarName == c.param2VarName { asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
if c.destVarName == c.param1VarName {
return asm, nil
}
if c.param1VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
if c.destVarKind == compiler.KindWord {
asm = append(asm, "\tlda #0")
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
}
return asm, nil
}
// Track if A is known to be 0 after low byte (avoids redundant lda #0 for high byte)
aIsZero := false
// If either param is literal 0, low byte result is always 0
param1LoIsZero := !c.param1IsVar && uint8(c.param1Value&0xFF) == 0
param2LoIsZero := !c.param2IsVar && uint8(c.param2Value&0xFF) == 0
// If param1 is literal $FF and param2 is a var, just load param2 ($FF AND a = a)
param1IsFF := !c.param1IsVar && uint8(c.param1Value&0xFF) == 0xFF
if param1LoIsZero || param2LoIsZero {
asm = append(asm, "\tlda #0")
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
aIsZero = true
} else if param1IsFF && c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param2VarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
} else { } else {
// Load param1 asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
if c.param1IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
}
// AND with param2 (skip if literal $FF, as and #$ff preserves accumulator)
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tand %s", c.param2VarName))
} else if uint8(c.param2Value&0xFF) != 0xFF {
asm = append(asm, fmt.Sprintf("\tand #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
} }
// AND with param2
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tand %s", c.param2VarName))
} else {
asm = append(asm, fmt.Sprintf("\tand #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
// If destination is word, handle high byte // If destination is word, handle high byte
if c.destVarKind == compiler.KindWord { if c.destVarKind == compiler.KindWord {
// Determine if param2 high byte is effectively 0 // Load high byte of param1
// (AND with 0 always yields 0 regardless of param1 high byte) if c.param1IsVar {
param2HiEffectiveZero := false if c.param1VarKind == compiler.KindWord {
if c.param2IsVar { asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
if c.param2VarKind == compiler.KindByte { } else {
param2HiEffectiveZero = true
}
} else {
param2HiEffectiveZero = ((c.param2Value >> 8) & 0xFF) == 0
}
if param2HiEffectiveZero {
if !aIsZero {
asm = append(asm, "\tlda #0") asm = append(asm, "\tlda #0")
} }
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
} else { } else {
// Load high byte of param1 hi := uint8((c.param1Value >> 8) & 0xFF)
if c.param1IsVar { asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
if c.param1VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
} else {
asm = append(asm, "\tlda #0")
}
} else {
hi := uint8((c.param1Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
}
// AND with high byte of param2
if c.param2IsVar {
if c.param2VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tand %s+1", c.param2VarName))
} else {
asm = append(asm, fmt.Sprintf("\tand #$%02x", uint8(c.param2Value&0xFF)))
}
} else {
hi := uint8((c.param2Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tand #$%02x", hi))
}
// Store high byte
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
} }
// AND with high byte of param2
if c.param2IsVar {
if c.param2VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tand %s+1", c.param2VarName))
} else {
asm = append(asm, "\tand #0")
}
} else {
hi := uint8((c.param2Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tand #$%02x", hi))
}
// Store high byte
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
} }
return asm, nil return asm, nil

View file

@ -71,6 +71,7 @@ func TestAndCommand_OldSyntax(t *testing.T) {
"\tand b", "\tand b",
"\tsta result", "\tsta result",
"\tlda #0", "\tlda #0",
"\tand #0",
"\tsta result+1", "\tsta result+1",
}, },
}, },
@ -112,7 +113,8 @@ func TestAndCommand_OldSyntax(t *testing.T) {
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
}, },
wantAsm: []string{ wantAsm: []string{
"\tlda b", "\tlda #$ff",
"\tand b",
"\tsta result", "\tsta result",
}, },
}, },
@ -180,7 +182,8 @@ func TestAndCommand_OldSyntax(t *testing.T) {
"\tlda wval", "\tlda wval",
"\tand bval", "\tand bval",
"\tsta result", "\tsta result",
"\tlda #0", "\tlda wval+1",
"\tand #0",
"\tsta result+1", "\tsta result+1",
}, },
}, },
@ -283,6 +286,7 @@ func TestAndCommand_NewSyntax(t *testing.T) {
"\tand b", "\tand b",
"\tsta result", "\tsta result",
"\tlda #0", "\tlda #0",
"\tand #0",
"\tsta result+1", "\tsta result+1",
}, },
}, },
@ -316,96 +320,6 @@ func TestAndCommand_NewSyntax(t *testing.T) {
"\tsta result", "\tsta result",
}, },
}, },
{
name: "byte & $FF -> byte (optimization: skip and #$ff)",
line: "result = a & $FF",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
},
},
{
name: "byte & $FF -> word (optimization: skip and #$ff)",
line: "result = a & $FF",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
"\tlda #0",
"\tsta result+1",
},
},
{
name: "byte & 0 -> byte (optimization: lda #0)",
line: "result = a & 0",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
},
},
{
name: "0 & byte -> byte (optimization: lda #0)",
line: "result = 0 & a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
},
},
{
name: "byte & 0 -> word (optimization: lda #0)",
line: "result = a & 0",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
"\tsta result+1",
},
},
{
name: "byte & $100 -> byte (optimization: lda #0, low byte of $100 is 0)",
line: "result = a & $100",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
},
},
{
name: "byte & $100 -> word (optimization: lda #0)",
line: "result = a & $100",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
"\tlda #0",
"\tand #$01",
"\tsta result+1",
},
},
{ {
name: "constant folding", name: "constant folding",
line: "result = 255 & 15", line: "result = 255 & 15",
@ -444,206 +358,6 @@ func TestAndCommand_NewSyntax(t *testing.T) {
"\tsta result", "\tsta result",
}, },
}, },
{
name: "byte & byte -> byte (same variable: a & a = a)",
line: "result = a & a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
},
},
{
name: "word & word -> word (same variable: x & x = x)",
line: "result = x & x",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda x",
"\tsta result",
"\tlda x+1",
"\tsta result+1",
},
},
{
name: "$FF & byte -> word (optimization: lda param2)",
line: "result = $FF & a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
"\tlda #0",
"\tsta result+1",
},
},
{
name: "byte & word -> byte (swap case)",
line: "result = bval & wval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("bval", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tand bval",
"\tsta result",
},
},
{
name: "byte & word -> word (swap case)",
line: "result = bval & wval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("bval", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tand bval",
"\tsta result",
"\tlda #0",
"\tsta result+1",
},
},
{
name: "word_const & byte -> byte",
line: "result = 300 & b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$2c",
"\tand b",
"\tsta result",
},
},
{
name: "word_const & byte -> word",
line: "result = 300 & b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$2c",
"\tand b",
"\tsta result",
"\tlda #0",
"\tsta result+1",
},
},
{
name: "byte & word_const -> byte",
line: "result = b & 300",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda b",
"\tand #$2c",
"\tsta result",
},
},
{
name: "byte & word_const -> word",
line: "result = b & 300",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda b",
"\tand #$2c",
"\tsta result",
"\tlda #0",
"\tand #$01",
"\tsta result+1",
},
},
{
name: "0 & byte -> word (optimization: lda #0, skip high byte)",
line: "result = 0 & a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
"\tsta result+1",
},
},
{
name: "self-assignment: word &= byte",
line: "wval = wval & bval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("bval", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tand bval",
"\tsta wval",
"\tlda #0",
"\tsta wval+1",
},
},
{
name: "self-assignment: word &= byte_const",
line: "wval = wval & 42",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tand #$2a",
"\tsta wval",
"\tlda #0",
"\tsta wval+1",
},
},
{
name: "self-assignment: word &= word_const",
line: "wval = wval & 300",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tand #$2c",
"\tsta wval",
"\tlda wval+1",
"\tand #$01",
"\tsta wval+1",
},
},
{
name: "self-assignment: word &= word",
line: "wval = wval & wval2",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("wval2", "", compiler.KindWord, 0x5678, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tand wval2",
"\tsta wval",
"\tlda wval+1",
"\tand wval2+1",
"\tsta wval+1",
},
},
{ {
name: "error: unknown destination", name: "error: unknown destination",
line: "unknown = a & b", line: "unknown = a & b",

View file

@ -16,8 +16,6 @@ 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
@ -34,6 +32,7 @@ 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
@ -46,32 +45,21 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
paramCount := len(params) paramCount := len(params)
// Check for REGISTER keyword // Validate parameter count
register := false if paramCount != 2 && paramCount != 4 && paramCount != 5 {
if paramCount >= 2 && strings.ToUpper(params[1]) == "REGISTER" { return fmt.Errorf("BYTE: wrong number of parameters (%d)", paramCount)
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 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
@ -79,16 +67,10 @@ 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]
@ -103,22 +85,14 @@ 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 { err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line)
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 == "@" { } else if operator == "@" {
if register { // BYTE varname @ address
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)
} }
@ -130,9 +104,7 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
} }
case 5: case 5:
if register { // BYTE CONST varname = value
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,66 +438,3 @@ 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

@ -11,9 +11,8 @@ import (
// MacroCommand handles macro invocations // MacroCommand handles macro invocations
// Syntax: @macroname(arg1, arg2, ...) // Syntax: @macroname(arg1, arg2, ...)
type MacroCommand struct { type MacroCommand struct {
macroName string macroName string
args []string args []string
pragmaSetIndex int
} }
func (c *MacroCommand) WillHandle(line preproc.Line) bool { func (c *MacroCommand) WillHandle(line preproc.Line) bool {
@ -31,12 +30,11 @@ func (c *MacroCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext)
c.macroName = name c.macroName = name
c.args = args c.args = args
c.pragmaSetIndex = line.PragmaSetIndex
return nil return nil
} }
func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
macroOutput, err := compiler.ExecuteMacro(c.macroName, c.args, ctx, c.pragmaSetIndex) macroOutput, err := compiler.ExecuteMacro(c.macroName, c.args, ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("macro %s: %w", c.macroName, err) return nil, fmt.Errorf("macro %s: %w", c.macroName, err)
} }

View file

@ -156,26 +156,6 @@ func (c *OrCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext)
} }
} }
// Normalize operands: leverage OR commutativity to swap if needed
// This ensures word operands are in param1 when mixed with bytes
// Makes code generation simpler and more consistent
param1IsByteSized := false
if c.param1IsVar {
param1IsByteSized = (c.param1VarKind == compiler.KindByte)
} else {
param1IsByteSized = ((c.param1Value >> 8) & 0xFF) == 0
}
param2IsWord := c.param2IsVar && c.param2VarKind == compiler.KindWord
if param1IsByteSized && param2IsWord {
// Swap param1 and param2
c.param1VarName, c.param2VarName = c.param2VarName, c.param1VarName
c.param1VarKind, c.param2VarKind = c.param2VarKind, c.param1VarKind
c.param1Value, c.param2Value = c.param2Value, c.param1Value
c.param1IsVar, c.param2IsVar = c.param2IsVar, c.param1IsVar
}
return nil return nil
} }
@ -200,73 +180,25 @@ func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
} }
// At least one param is a variable - generate OR code // At least one param is a variable - generate OR code
// Load param1
// Same variable on both sides: a | a = a if c.param1IsVar {
if c.param1IsVar && c.param2IsVar && c.param1VarName == c.param2VarName { asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
if c.destVarName == c.param1VarName {
return asm, nil
}
if c.param1VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
if c.destVarKind == compiler.KindWord {
asm = append(asm, "\tlda #0")
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
}
return asm, nil
}
// If either param is literal $FF, low byte result is always $FF
param1IsFF := !c.param1IsVar && uint8(c.param1Value&0xFF) == 0xFF
param2IsFF := !c.param2IsVar && uint8(c.param2Value&0xFF) == 0xFF
if param1IsFF || param2IsFF {
asm = append(asm, "\tlda #$ff")
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
} else if !c.param1IsVar && uint8(c.param1Value&0xFF) == 0 && c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param2VarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
} else { } else {
// Load param1 asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
if c.param1IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
}
// OR with param2 (skip if literal 0, as ora #0 is a no-op)
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tora %s", c.param2VarName))
} else if uint8(c.param2Value&0xFF) != 0 {
asm = append(asm, fmt.Sprintf("\tora #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
} }
// OR with param2
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tora %s", c.param2VarName))
} else {
asm = append(asm, fmt.Sprintf("\tora #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
// If destination is word, handle high byte // If destination is word, handle high byte
if c.destVarKind == compiler.KindWord { if c.destVarKind == compiler.KindWord {
// Optimization: skip high byte for self-assignment when param2 is byte-sized
// e.g., word_var = word_var | byte_var would just copy high byte to itself
if c.destVarName == c.param1VarName {
param2IsByteSized := false
if c.param2IsVar {
param2IsByteSized = (c.param2VarKind == compiler.KindByte)
} else {
param2IsByteSized = ((c.param2Value >> 8) & 0xFF) == 0
}
if param2IsByteSized {
return asm, nil
}
}
// Load high byte of param1 // Load high byte of param1
if c.param1IsVar { if c.param1IsVar {
if c.param1VarKind == compiler.KindWord { if c.param1VarKind == compiler.KindWord {
@ -279,18 +211,16 @@ func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
} }
// OR with high byte of param2 (skip if literal 0, as ora #0 is a no-op) // OR with high byte of param2
if c.param2IsVar { if c.param2IsVar {
if c.param2VarKind == compiler.KindWord { if c.param2VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tora %s+1", c.param2VarName)) asm = append(asm, fmt.Sprintf("\tora %s+1", c.param2VarName))
} else {
asm = append(asm, "\tora #0")
} }
// Skip ora when param2 is byte var (high byte is 0)
} else { } else {
hi := uint8((c.param2Value >> 8) & 0xFF) hi := uint8((c.param2Value >> 8) & 0xFF)
if hi != 0 { asm = append(asm, fmt.Sprintf("\tora #$%02x", hi))
asm = append(asm, fmt.Sprintf("\tora #$%02x", hi))
}
// Skip ora when param2 const high byte is 0
} }
// Store high byte // Store high byte

View file

@ -71,6 +71,7 @@ func TestOrCommand_OldSyntax(t *testing.T) {
"\tora b", "\tora b",
"\tsta result", "\tsta result",
"\tlda #0", "\tlda #0",
"\tora #0",
"\tsta result+1", "\tsta result+1",
}, },
}, },
@ -182,6 +183,7 @@ func TestOrCommand_OldSyntax(t *testing.T) {
"\tora bval", "\tora bval",
"\tsta result", "\tsta result",
"\tlda wval+1", "\tlda wval+1",
"\tora #0",
"\tsta result+1", "\tsta result+1",
}, },
}, },
@ -284,6 +286,7 @@ func TestOrCommand_NewSyntax(t *testing.T) {
"\tora b", "\tora b",
"\tsta result", "\tsta result",
"\tlda #0", "\tlda #0",
"\tora #0",
"\tsta result+1", "\tsta result+1",
}, },
}, },
@ -317,32 +320,6 @@ func TestOrCommand_NewSyntax(t *testing.T) {
"\tsta result", "\tsta result",
}, },
}, },
{
name: "byte | 0 -> byte (optimization: skip ora #0)",
line: "result = a | 0",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
},
},
{
name: "byte | 0 -> word (optimization: skip ora #0)",
line: "result = a | 0",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
"\tlda #0",
"\tsta result+1",
},
},
{ {
name: "constant folding", name: "constant folding",
line: "result = 15 | 240", line: "result = 15 | 240",
@ -381,294 +358,6 @@ func TestOrCommand_NewSyntax(t *testing.T) {
"\tsta result", "\tsta result",
}, },
}, },
{
name: "byte | $FF -> byte (optimization: lda #$ff)",
line: "result = a | $FF",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$ff",
"\tsta result",
},
},
{
name: "$FF | byte -> byte (optimization: lda #$ff)",
line: "result = $FF | a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$ff",
"\tsta result",
},
},
{
name: "byte | $FF -> word (optimization: lda #$ff)",
line: "result = a | $FF",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$ff",
"\tsta result",
"\tlda #0",
"\tsta result+1",
},
},
{
name: "byte | byte -> byte (same variable: a | a = a)",
line: "result = a | a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
},
},
{
name: "0 | byte -> word (optimization: skip ora #0)",
line: "result = 0 | a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
"\tlda #$00",
"\tsta result+1",
},
},
{
name: "byte | word -> byte (swap case)",
line: "result = bval | wval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("bval", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tora bval",
"\tsta result",
},
},
{
name: "byte | word -> word (swap case)",
line: "result = bval | wval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("bval", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tora bval",
"\tsta result",
"\tlda wval+1",
"\tsta result+1",
},
},
{
name: "$FF | word -> word",
line: "result = $FF | wval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$ff",
"\tsta result",
"\tlda wval+1",
"\tsta result+1",
},
},
{
name: "word | $FF -> word",
line: "result = wval | $FF",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$ff",
"\tsta result",
"\tlda wval+1",
"\tsta result+1",
},
},
{
name: "$FF00 | byte -> byte",
line: "result = $FF00 | b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda b",
"\tsta result",
},
},
{
name: "$FF00 | byte -> word",
line: "result = $FF00 | b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda b",
"\tsta result",
"\tlda #$ff",
"\tsta result+1",
},
},
{
name: "byte | word_const -> byte",
line: "result = b | 300",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda b",
"\tora #$2c",
"\tsta result",
},
},
{
name: "byte | word_const -> word",
line: "result = b | 300",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda b",
"\tora #$2c",
"\tsta result",
"\tlda #0",
"\tora #$01",
"\tsta result+1",
},
},
{
name: "word_const | byte -> byte",
line: "result = 300 | b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$2c",
"\tora b",
"\tsta result",
},
},
{
name: "word_const | byte -> word",
line: "result = 300 | b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #$2c",
"\tora b",
"\tsta result",
"\tlda #$01",
"\tsta result+1",
},
},
{
name: "self-assignment: word |= byte (optimization: skip high byte entirely)",
line: "wval = wval | bval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("bval", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tora bval",
"\tsta wval",
},
},
{
name: "self-assignment reversed: word |= byte (optimization via swap)",
line: "wval = bval | wval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("bval", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tora bval",
"\tsta wval",
},
},
{
name: "self-assignment: word |= byte_const (optimization: skip high byte entirely)",
line: "wval = wval | 42",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tora #$2a",
"\tsta wval",
},
},
{
name: "self-assignment: word |= word_const (no optimization: high byte needed)",
line: "wval = wval | 300",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tora #$2c",
"\tsta wval",
"\tlda wval+1",
"\tora #$01",
"\tsta wval+1",
},
},
{
name: "self-assignment: word |= word (no optimization: both high bytes needed)",
line: "wval = wval | wval2",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("wval2", "", compiler.KindWord, 0x5678, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda wval",
"\tora wval2",
"\tsta wval",
"\tlda wval+1",
"\tora wval2+1",
"\tsta wval+1",
},
},
{
name: "word | word -> word (same variable: x | x = x)",
line: "result = x | x",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda x",
"\tsta result",
"\tlda x+1",
"\tsta result+1",
},
},
{ {
name: "error: unknown destination", name: "error: unknown destination",
line: "unknown = a | b", line: "unknown = a | b",

View file

@ -106,29 +106,29 @@ func (c *PointerCommand) Generate(ctx *compiler.CompilerContext) ([]string, erro
// Label reference // Label reference
if c.isLabel { if c.isLabel {
asm = append(asm, fmt.Sprintf("\tlda #<%s", c.targetLabel)) asm = append(asm, fmt.Sprintf("\tldx #<%s", c.targetLabel))
asm = append(asm, fmt.Sprintf("\tsta %s", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetLabel)) asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetLabel))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName)) asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
return asm, nil return asm, nil
} }
// Variable reference // Variable reference
if c.isVar { if c.isVar {
asm = append(asm, fmt.Sprintf("\tlda #<%s", c.targetVarName)) asm = append(asm, fmt.Sprintf("\tldx #<%s", c.targetVarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetVarName)) asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetVarName))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName)) asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
return asm, nil return asm, nil
} }
// Numeric address - create temp label // Numeric address - create temp label
tempLabel := ctx.GeneralStack.Push() tempLabel := ctx.GeneralStack.Push()
asm = append(asm, fmt.Sprintf("%s = %d", tempLabel, c.targetAddress)) asm = append(asm, fmt.Sprintf("%s = %d", tempLabel, c.targetAddress))
asm = append(asm, fmt.Sprintf("\tlda #<%s", tempLabel)) asm = append(asm, fmt.Sprintf("\tldx #<%s", tempLabel))
asm = append(asm, fmt.Sprintf("\tsta %s", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tlda #>%s", tempLabel)) asm = append(asm, fmt.Sprintf("\tlda #>%s", tempLabel))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName)) asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
return asm, nil return asm, nil
} }

View file

@ -1,131 +0,0 @@
package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
func TestPointerCommand_Generate(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
}{
{
name: "pointer to label — uses A only",
line: "POINTER ptr -> TARGET",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #<TARGET",
"\tsta ptr",
"\tlda #>TARGET",
"\tsta ptr+1",
},
},
{
name: "pointer to variable — uses A only",
line: "POINTER ptr TO targetVar",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("targetVar", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #<targetVar",
"\tsta ptr",
"\tlda #>targetVar",
"\tsta ptr+1",
},
},
{
name: "pointer to numeric address — uses A only",
line: "POINTER ptr -> 53280",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
tt.setupVars(ctx.SymbolTable)
cmd := &PointerCommand{}
line := preproc.Line{
Text: tt.line,
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if tt.name == "pointer to numeric address — uses A only" {
foundLo := false
foundHi := false
foundSta := false
for _, a := range asm {
if strings.Contains(a, "lda #<") {
foundLo = true
}
if strings.Contains(a, "lda #>") {
foundHi = true
}
if strings.Contains(a, "sta ptr+1") {
foundSta = true
}
}
if !foundLo || !foundHi || !foundSta {
t.Errorf("expected A-only pattern (lda #< / sta ptr / lda #> / sta ptr+1), got:\n%s", strings.Join(asm, "\n"))
}
return
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
func TestPointerCommand_NoXRegister(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
cmd := &PointerCommand{}
line := preproc.Line{
Text: "POINTER ptr -> $0400",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
joined := strings.Join(asm, "\n")
if strings.Contains(joined, "ldx") || strings.Contains(joined, "stx") {
t.Errorf("POINTER should not use X register, got:\n%s", joined)
}
}

View file

@ -38,7 +38,7 @@ type PokeCommand struct {
} }
func (c *PokeCommand) WillHandle(line preproc.Line) bool { func (c *PokeCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text)) params, err := utils.ParseParams(line.Text)
if err != nil || len(params) != 4 { if err != nil || len(params) != 4 {
return false return false
} }
@ -62,7 +62,7 @@ func (c *PokeCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
// Store pragma set for Generate phase // Store pragma set for Generate phase
c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text)) params, err := utils.ParseParams(line.Text)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,987 +0,0 @@
package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
// setupPokeVars adds all needed symbols to a symbol table for POKE/POKEW tests.
func setupPokeVars(st *compiler.SymbolTable) {
st.AddConst("vic2", "", compiler.KindWord, 0xd000, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("addrvar", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 2})
st.AddVar("valvar", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 3})
st.AddVar("wvalvar", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 4})
st.AddVar("waddrvar", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 5})
st.AddAbsolute("zpptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 6})
st.AddVar("offsvar", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 7})
st.AddAbsolute("byaddr", "", compiler.KindByte, 0x40, preproc.Line{Filename: "test.c65", LineNo: 8})
}
func newCtx() *compiler.CompilerContext {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
setupPokeVars(ctx.SymbolTable)
return ctx
}
func newLine(text string, pragma *preproc.Pragma) preproc.Line {
return preproc.Line{
Text: text,
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
}
// =============================================================================
// POKE comma-spacing tests (constant address + literal value — Case 4)
// =============================================================================
func TestPokeCommaSpacing(t *testing.T) {
expectedAsm := []string{
"\tlda #5",
"\tsta 53280",
}
tests := []struct {
name string
line string
}{
{"space before and after comma", "POKE $d020 , 5"},
{"no space before or after comma", "POKE $d020,5"},
{"no space before comma, space after", "POKE $d020, 5"},
{"space before comma, no space after", "POKE $d020 ,5"},
{"no space comma, expression address", "POKE $d020+0, 5"},
{"WITH keyword", "POKE $d020 WITH 5"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, expectedAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(expectedAsm, "\n"))
}
})
}
}
// =============================================================================
// POKEW comma-spacing tests (constant address + literal value — Case 4)
// =============================================================================
func TestPokeWCommaSpacing(t *testing.T) {
expectedAsm := []string{
"\tlda #$65",
"\tsta 53280",
"\tlda #$00",
"\tsta 53281",
}
tests := []struct {
name string
line string
}{
{"space before and after comma", "POKEW $d020 , 101"},
{"no space before or after comma", "POKEW $d020,101"},
{"no space before comma, space after", "POKEW $d020, 101"},
{"space before comma, no space after", "POKEW $d020 ,101"},
{"WITH keyword", "POKEW $d020 WITH 101"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeWCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, expectedAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(expectedAsm, "\n"))
}
})
}
}
// =============================================================================
// POKE Case 4 — Direct addressing (expression/constant address)
// =============================================================================
func TestPokeDirectAddr(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "expression constant + expression value, no-space comma",
line: "POKE vic2+15,5+2",
wantAsm: []string{
"\tlda #7",
"\tsta 53263",
},
},
{
name: "variable value, no-space comma",
line: "POKE $d020,valvar",
wantAsm: []string{
"\tlda valvar",
"\tsta 53280",
},
},
{
name: "variable value, space before comma",
line: "POKE $d020 ,valvar",
wantAsm: []string{
"\tlda valvar",
"\tsta 53280",
},
},
{
name: "variable value, space after comma",
line: "POKE $d020, valvar",
wantAsm: []string{
"\tlda valvar",
"\tsta 53280",
},
},
{
name: "expression address + var value, no-space comma",
line: "POKE vic2+15,valvar",
wantAsm: []string{
"\tlda valvar",
"\tsta 53263",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// POKE Case 1 — ZP pointer (indexed indirect addressing)
// =============================================================================
func TestPokeZPPointer(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "no offset, literal value, no-space comma",
line: "POKE zpptr,10",
wantAsm: []string{
"\tldy #0",
"\tlda #10",
"\tsta (zpptr),y",
},
},
{
name: "no offset, var value, no-space comma",
line: "POKE zpptr,valvar",
wantAsm: []string{
"\tldy #0",
"\tlda valvar",
"\tsta (zpptr),y",
},
},
{
name: "literal offset, literal value, no-space comma",
line: "POKE zpptr[5],10",
wantAsm: []string{
"\tldy #5",
"\tlda #10",
"\tsta (zpptr),y",
},
},
{
name: "literal offset, var value, no-space comma",
line: "POKE zpptr[5],valvar",
wantAsm: []string{
"\tldy #5",
"\tlda valvar",
"\tsta (zpptr),y",
},
},
{
name: "var offset, literal value, no-space comma",
line: "POKE zpptr[offsvar],10",
wantAsm: []string{
"\tldy offsvar",
"\tlda #10",
"\tsta (zpptr),y",
},
},
{
name: "var offset, var value, no-space comma",
line: "POKE zpptr[offsvar],valvar",
wantAsm: []string{
"\tldy offsvar",
"\tlda valvar",
"\tsta (zpptr),y",
},
},
{
name: "literal offset, var value, spaced comma",
line: "POKE zpptr[5] , valvar",
wantAsm: []string{
"\tldy #5",
"\tlda valvar",
"\tsta (zpptr),y",
},
},
{
name: "word variable value (uses low byte), no-space comma",
line: "POKE zpptr[5],wvalvar",
wantAsm: []string{
"\tldy #5",
"\tlda wvalvar",
"\tsta (zpptr),y",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// POKE Case 2 — Byte variable address (self-modifying code)
// =============================================================================
func TestPokeSMByteAddr(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "literal value, no-space comma",
line: "POKE addrvar,10",
wantAsm: []string{
"\tlda addrvar",
"\tsta _L1+1",
"\tlda #10",
"_L1",
"\tsta $ff",
},
},
{
name: "var value, no-space comma",
line: "POKE addrvar,valvar",
wantAsm: []string{
"\tlda addrvar",
"\tsta _L1+1",
"\tlda valvar",
"_L1",
"\tsta $ff",
},
},
{
name: "word var value (uses low byte), no-space comma",
line: "POKE addrvar,wvalvar",
wantAsm: []string{
"\tlda addrvar",
"\tsta _L1+1",
"\tlda wvalvar",
"_L1",
"\tsta $ff",
},
},
{
name: "literal value, spaced comma",
line: "POKE addrvar , 10",
wantAsm: []string{
"\tlda addrvar",
"\tsta _L1+1",
"\tlda #10",
"_L1",
"\tsta $ff",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// POKE Case 3 — Word variable address (self-modifying code)
// =============================================================================
func TestPokeSMWordAddr(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "literal value, no-space comma",
line: "POKE waddrvar,10",
wantAsm: []string{
"\tlda waddrvar",
"\tsta _L1+1",
"\tlda waddrvar+1",
"\tsta _L1+2",
"\tlda #10",
"_L1",
"\tsta $ffff",
},
},
{
name: "var value, no-space comma",
line: "POKE waddrvar,valvar",
wantAsm: []string{
"\tlda waddrvar",
"\tsta _L1+1",
"\tlda waddrvar+1",
"\tsta _L1+2",
"\tlda valvar",
"_L1",
"\tsta $ffff",
},
},
{
name: "word var value (uses low byte), no-space comma",
line: "POKE waddrvar,wvalvar",
wantAsm: []string{
"\tlda waddrvar",
"\tsta _L1+1",
"\tlda waddrvar+1",
"\tsta _L1+2",
"\tlda wvalvar",
"_L1",
"\tsta $ffff",
},
},
{
name: "literal value, spaced comma",
line: "POKE waddrvar , 10",
wantAsm: []string{
"\tlda waddrvar",
"\tsta _L1+1",
"\tlda waddrvar+1",
"\tsta _L1+2",
"\tlda #10",
"_L1",
"\tsta $ffff",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// POKEW Case 4 — Direct addressing
// =============================================================================
func TestPokeWDirectAddr(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "literal value, no-space comma",
line: "POKEW $d020,101",
wantAsm: []string{
"\tlda #$65",
"\tsta 53280",
"\tlda #$00",
"\tsta 53281",
},
},
{
name: "literal value >255, no-space comma",
line: "POKEW $d020,$1234",
wantAsm: []string{
"\tlda #$34",
"\tsta 53280",
"\tlda #$12",
"\tsta 53281",
},
},
{
name: "word var value, no-space comma",
line: "POKEW $d020,wvalvar",
wantAsm: []string{
"\tlda wvalvar",
"\tsta 53280",
"\tlda wvalvar+1",
"\tsta 53281",
},
},
{
name: "word var value, spaced comma",
line: "POKEW $d020 , wvalvar",
wantAsm: []string{
"\tlda wvalvar",
"\tsta 53280",
"\tlda wvalvar+1",
"\tsta 53281",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeWCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// POKEW Case 1 — ZP pointer (indexed indirect, two stores)
// =============================================================================
func TestPokeWZPPointer(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "no offset, literal value, no-space comma",
line: "POKEW zpptr,101",
wantAsm: []string{
"\tldy #0",
"\tlda #$65",
"\tsta (zpptr),y",
"\tiny",
"\tlda #$00",
"\tsta (zpptr),y",
},
},
{
name: "no offset, word var value, no-space comma",
line: "POKEW zpptr,wvalvar",
wantAsm: []string{
"\tldy #0",
"\tlda wvalvar",
"\tsta (zpptr),y",
"\tiny",
"\tlda wvalvar+1",
"\tsta (zpptr),y",
},
},
{
name: "literal offset, literal value, no-space comma",
line: "POKEW zpptr[5],101",
wantAsm: []string{
"\tldy #5",
"\tlda #$65",
"\tsta (zpptr),y",
"\tiny",
"\tlda #$00",
"\tsta (zpptr),y",
},
},
{
name: "literal offset, word var value, no-space comma",
line: "POKEW zpptr[5],wvalvar",
wantAsm: []string{
"\tldy #5",
"\tlda wvalvar",
"\tsta (zpptr),y",
"\tiny",
"\tlda wvalvar+1",
"\tsta (zpptr),y",
},
},
{
name: "literal offset, literal value, spaced comma",
line: "POKEW zpptr[5] , 101",
wantAsm: []string{
"\tldy #5",
"\tlda #$65",
"\tsta (zpptr),y",
"\tiny",
"\tlda #$00",
"\tsta (zpptr),y",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeWCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// POKEW Case 2 — Byte variable address (zero-page indexed)
// =============================================================================
func TestPokeWByteAddr(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "literal value, no-space comma",
line: "POKEW byaddr,101",
wantAsm: []string{
"\tldx byaddr",
"\tlda #$65",
"\tsta $00,x",
"\tinx",
"\tlda #$00",
"\tsta $00,x",
},
},
{
name: "word var value, no-space comma",
line: "POKEW byaddr,wvalvar",
wantAsm: []string{
"\tldx byaddr",
"\tlda wvalvar",
"\tsta $00,x",
"\tinx",
"\tlda wvalvar+1",
"\tsta $00,x",
},
},
{
name: "literal value, spaced comma",
line: "POKEW byaddr , 101",
wantAsm: []string{
"\tldx byaddr",
"\tlda #$65",
"\tsta $00,x",
"\tinx",
"\tlda #$00",
"\tsta $00,x",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeWCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// POKEW Case 3 — Word variable address (self-modifying code)
// =============================================================================
func TestPokeWSMWordAddr(t *testing.T) {
tests := []struct {
name string
line string
wantAsm []string
}{
{
name: "literal value, no-space comma",
line: "POKEW waddrvar,101",
wantAsm: []string{
"\tlda waddrvar",
"\tsta _L1+1",
"\tsta _L2+1",
"\tlda waddrvar+1",
"\tsta _L1+2",
"\tsta _L2+2",
"\tlda #$65",
"_L1",
"\tsta $ffff",
"\tinc _L2+1",
"\tbne _L2",
"\tinc _L2+2",
"_L2",
"\tlda #$00",
"\tsta $ffff",
},
},
{
name: "word var value, no-space comma",
line: "POKEW waddrvar,wvalvar",
wantAsm: []string{
"\tlda waddrvar",
"\tsta _L1+1",
"\tsta _L2+1",
"\tlda waddrvar+1",
"\tsta _L1+2",
"\tsta _L2+2",
"\tlda wvalvar",
"_L1",
"\tsta $ffff",
"\tinc _L2+1",
"\tbne _L2",
"\tinc _L2+2",
"_L2",
"\tlda wvalvar+1",
"\tsta $ffff",
},
},
{
name: "literal value, spaced comma",
line: "POKEW waddrvar , 101",
wantAsm: []string{
"\tlda waddrvar",
"\tsta _L1+1",
"\tsta _L2+1",
"\tlda waddrvar+1",
"\tsta _L1+2",
"\tsta _L2+2",
"\tlda #$65",
"_L1",
"\tsta $ffff",
"\tinc _L2+1",
"\tbne _L2",
"\tinc _L2+2",
"_L2",
"\tlda #$00",
"\tsta $ffff",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
cmd := &PokeWCommand{}
line := newLine(tt.line, ctx.Pragma)
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// =============================================================================
// Error cases
// =============================================================================
func TestPokeErrors(t *testing.T) {
tests := []struct {
name string
line string
wantErr string
}{
{
name: "offset on non-ZP word pointer",
line: "POKE waddrvar[5],10",
wantErr: "POKE: offset",
},
{
name: "value out of byte range",
line: "POKE $d020,256",
wantErr: "out of byte range",
},
{
name: "POKEW with byte variable as value",
line: "POKEW $d020,valvar",
wantErr: "cannot use byte variable",
},
{
name: "POKEW self-referential (zp pointer == value)",
line: "POKEW zpptr,zpptr",
wantErr: "writing pointer",
},
{
name: "invalid separator",
line: "POKE $d020 WRONG 5",
wantErr: "must be 'WITH' or ','",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := newCtx()
if strings.HasPrefix(tt.line, "POKEW") {
cmd := &PokeWCommand{}
line := newLine(tt.line, ctx.Pragma)
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Interpret() expected error but got nil")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("Interpret() error = %q, want containing %q", err.Error(), tt.wantErr)
}
return
}
cmd := &PokeCommand{}
line := newLine(tt.line, ctx.Pragma)
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Interpret() expected error but got nil")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("Interpret() error = %q, want containing %q", err.Error(), tt.wantErr)
}
})
}
}
func TestPokePragmaImmutable(t *testing.T) {
t.Run("SM byte addr with USE_IMMUTABLE_CODE", func(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "1")
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("addrvar", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
cmd := &PokeCommand{}
line := preproc.Line{
Text: "POKE addrvar,10",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
_, err := cmd.Generate(ctx)
if err == nil {
t.Fatal("Generate() expected error with USE_IMMUTABLE_CODE")
}
if !strings.Contains(err.Error(), "USE_IMMUTABLE_CODE") {
t.Errorf("Generate() error = %q, want containing USE_IMMUTABLE_CODE", err.Error())
}
})
t.Run("SM word addr with USE_IMMUTABLE_CODE", func(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "1")
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("waddrvar", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
cmd := &PokeCommand{}
line := preproc.Line{
Text: "POKE waddrvar,10",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
_, err := cmd.Generate(ctx)
if err == nil {
t.Fatal("Generate() expected error with USE_IMMUTABLE_CODE")
}
if !strings.Contains(err.Error(), "USE_IMMUTABLE_CODE") {
t.Errorf("Generate() error = %q, want containing USE_IMMUTABLE_CODE", err.Error())
}
})
t.Run("ZP pointer is allowed with USE_IMMUTABLE_CODE", func(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "1")
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddAbsolute("zpptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
cmd := &PokeCommand{}
line := preproc.Line{
Text: "POKE zpptr,10",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v (ZP pointers should be allowed)", err)
}
expected := []string{
"\tldy #0",
"\tlda #10",
"\tsta (zpptr),y",
}
if !equalAsm(asm, expected) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(expected, "\n"))
}
})
t.Run("direct addr is allowed with USE_IMMUTABLE_CODE", func(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "1")
ctx := compiler.NewCompilerContext(pragma)
cmd := &PokeCommand{}
line := preproc.Line{
Text: "POKE $d020,5",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v (direct addr should be allowed)", err)
}
expected := []string{
"\tlda #5",
"\tsta 53280",
}
if !equalAsm(asm, expected) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(expected, "\n"))
}
})
}

View file

@ -37,7 +37,7 @@ type PokeWCommand struct {
} }
func (c *PokeWCommand) WillHandle(line preproc.Line) bool { func (c *PokeWCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text)) params, err := utils.ParseParams(line.Text)
if err != nil || len(params) != 4 { if err != nil || len(params) != 4 {
return false return false
} }
@ -61,7 +61,7 @@ func (c *PokeWCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContex
// Store pragma set for Generate phase // Store pragma set for Generate phase
c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text)) params, err := utils.ParseParams(line.Text)
if err != nil { if err != nil {
return err return err
} }

View file

@ -47,11 +47,6 @@ 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,25 +636,3 @@ 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

@ -200,34 +200,18 @@ func (c *XorCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
} }
// At least one param is a variable - generate XOR code // At least one param is a variable - generate XOR code
// Load param1
// Same variable on both sides: a ^ a = 0 if c.param1IsVar {
if c.param1IsVar && c.param2IsVar && c.param1VarName == c.param2VarName { asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
asm = append(asm, "\tlda #0") } else {
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
if c.destVarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
} }
// If param1 is literal 0, just load param2 directly (0 XOR a = a) // XOR with param2
if !c.param1IsVar && uint8(c.param1Value&0xFF) == 0 && c.param2IsVar { if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param2VarName)) asm = append(asm, fmt.Sprintf("\teor %s", c.param2VarName))
} else { } else {
// Load param1 asm = append(asm, fmt.Sprintf("\teor #$%02x", uint8(c.param2Value&0xFF)))
if c.param1IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
}
// XOR with param2 (skip if literal 0, as eor #0 is a no-op)
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\teor %s", c.param2VarName))
} else if uint8(c.param2Value&0xFF) != 0 {
asm = append(asm, fmt.Sprintf("\teor #$%02x", uint8(c.param2Value&0xFF)))
}
} }
// Store low byte // Store low byte

View file

@ -463,44 +463,6 @@ func TestXorCommand_NewSyntax(t *testing.T) {
"\tsta result", "\tsta result",
}, },
}, },
{
name: "byte ^ 0 -> byte (optimization: skip eor #0)",
line: "result = a ^ 0",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
},
},
{
name: "0 ^ byte -> byte (optimization: skip eor #0)",
line: "result = 0 ^ a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
},
},
{
name: "byte ^ 0 -> word (optimization: skip eor #0)",
line: "result = a ^ 0",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda a",
"\tsta result",
"\tlda #0",
"\tsta result+1",
},
},
{ {
name: "constant folding", name: "constant folding",
line: "result = 255 ^ 170", line: "result = 255 ^ 170",
@ -539,31 +501,6 @@ func TestXorCommand_NewSyntax(t *testing.T) {
"\tsta result", "\tsta result",
}, },
}, },
{
name: "byte ^ byte -> byte (same variable: a ^ a = 0)",
line: "result = a ^ a",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
},
},
{
name: "word ^ word -> word (same variable: x ^ x = 0)",
line: "result = x ^ x",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #0",
"\tsta result",
"\tsta result+1",
},
},
{ {
name: "error: unknown destination", name: "error: unknown destination",
line: "unknown = a ^ b", line: "unknown = a ^ b",

View file

@ -14,11 +14,10 @@ import (
type Compiler struct { type Compiler struct {
ctx *CompilerContext ctx *CompilerContext
registry *CommandRegistry registry *CommandRegistry
deferredAsm []optimizer.SourceLine // 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
} }
// NewCompiler creates a new compiler with initialized context and registry // NewCompiler creates a new compiler with initialized context and registry
@ -39,60 +38,16 @@ func (c *Compiler) Registry() *CommandRegistry {
return c.registry return c.registry
} }
// generatedLine tags a line as compiler-generated (optimizable).
func generatedLine(text string) optimizer.SourceLine {
return optimizer.SourceLine{Text: text, Origin: optimizer.OriginGenerated}
}
// generatedLines tags a batch of compiler-generated lines.
func generatedLines(lines []string) []optimizer.SourceLine {
out := make([]optimizer.SourceLine, len(lines))
for i, l := range lines {
out[i] = generatedLine(l)
}
return out
}
// asmSourceLine tags a line as verbatim ASM block content.
func asmSourceLine(text string) optimizer.SourceLine {
return optimizer.SourceLine{Text: text, Origin: optimizer.OriginAsm}
}
// scriptSourceLine tags a line as verbatim SCRIPT print() output.
func scriptSourceLine(text string) optimizer.SourceLine {
return optimizer.SourceLine{Text: text, Origin: optimizer.OriginScript}
}
// scriptSourceLines tags a batch of SCRIPT print() output lines.
func scriptSourceLines(lines []string) []optimizer.SourceLine {
out := make([]optimizer.SourceLine, len(lines))
for i, l := range lines {
out[i] = scriptSourceLine(l)
}
return out
}
// asmSourceLines tags a batch of verbatim ASM block lines.
func asmSourceLines(lines []string) []optimizer.SourceLine {
out := make([]optimizer.SourceLine, len(lines))
for i, l := range lines {
out[i] = asmSourceLine(l)
}
return out
}
// Compile processes preprocessed lines and generates assembly output // Compile processes preprocessed lines and generates assembly output
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
var codeOutput []optimizer.SourceLine var codeOutput []string
var lastKind = preproc.Source var lastKind = preproc.Source
var scriptBuffer []preproc.Line var scriptBuffer []string
var scriptIsLibrary bool var scriptIsLibrary bool
var macroBuffer []string var macroBuffer []string
var currentMacroName string var currentMacroName string
var currentMacroParams []string var currentMacroParams []string
var currentMacroSourceFile string var currentAsmTarget *[]string // nil = no active ASM block, or points to target slice
var currentMacroStartLine int
var currentAsmTarget *[]optimizer.SourceLine // nil = no active ASM block, or points to target slice
// Reset deferred ASM storage for this compilation // Reset deferred ASM storage for this compilation
c.deferredAsm = nil c.deferredAsm = nil
@ -106,12 +61,12 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("script execution failed: %w", err) return nil, fmt.Errorf("script execution failed: %w", err)
} }
codeOutput = append(codeOutput, scriptSourceLines(scriptOutput)...) codeOutput = append(codeOutput, scriptOutput...)
scriptBuffer = nil scriptBuffer = nil
if scriptIsLibrary { if scriptIsLibrary {
codeOutput = append(codeOutput, generatedLine("; ENDSCRIPT LIBRARY")) codeOutput = append(codeOutput, "; ENDSCRIPT LIBRARY")
} else { } else {
codeOutput = append(codeOutput, generatedLine("; ENDSCRIPT")) codeOutput = append(codeOutput, "; ENDSCRIPT")
} }
} }
@ -119,24 +74,20 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
if lastKind == preproc.ScriptMacroDef { if lastKind == preproc.ScriptMacroDef {
if currentMacroName != "" { if currentMacroName != "" {
c.ctx.ScriptMacros[currentMacroName] = &ScriptMacro{ c.ctx.ScriptMacros[currentMacroName] = &ScriptMacro{
Name: currentMacroName, Name: currentMacroName,
Params: currentMacroParams, Params: currentMacroParams,
Body: macroBuffer, Body: macroBuffer,
SourceFile: currentMacroSourceFile,
StartLine: currentMacroStartLine,
} }
codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))) codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
} }
macroBuffer = nil macroBuffer = nil
currentMacroName = "" currentMacroName = ""
currentMacroParams = nil currentMacroParams = nil
currentMacroSourceFile = ""
currentMacroStartLine = 0
} }
// Close previous Assembler block // Close previous Assembler block
if lastKind == preproc.Assembler && currentAsmTarget != nil { if lastKind == preproc.Assembler && currentAsmTarget != nil {
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine("; ENDASM")) *currentAsmTarget = append(*currentAsmTarget, "; ENDASM")
currentAsmTarget = nil currentAsmTarget = nil
} }
@ -144,25 +95,25 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
if line.Kind == preproc.Assembler { if line.Kind == preproc.Assembler {
// Check if ASM block should be deferred to end // Check if ASM block should be deferred to end
pragmaSet := c.ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) pragmaSet := c.ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
asmAfterVars := pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "" && asmAfterVars := pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "" &&
pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "0" pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "0"
if asmAfterVars { if asmAfterVars {
// Add inline comment and defer ASM block // Add inline comment and defer ASM block
codeOutput = append(codeOutput, generatedLine("; ASM block deferred to end of source")) codeOutput = append(codeOutput, "; ASM block deferred to end of source")
c.deferredAsm = append(c.deferredAsm, c.deferredAsm = append(c.deferredAsm,
asmSourceLine(fmt.Sprintf("; ASM Block from %s, Line %d", line.Filename, line.LineNo))) fmt.Sprintf("; ASM Block from %s, Line %d", line.Filename, line.LineNo))
currentAsmTarget = &c.deferredAsm currentAsmTarget = &c.deferredAsm
} else { } else {
// Normal ASM block // Normal ASM block
codeOutput = append(codeOutput, generatedLine("; ASM")) codeOutput = append(codeOutput, "; ASM")
currentAsmTarget = &codeOutput currentAsmTarget = &codeOutput
} }
} else if line.Kind == preproc.Script { } else if line.Kind == preproc.Script {
codeOutput = append(codeOutput, generatedLine("; SCRIPT")) codeOutput = append(codeOutput, "; SCRIPT")
scriptIsLibrary = false scriptIsLibrary = false
} else if line.Kind == preproc.ScriptLibrary { } else if line.Kind == preproc.ScriptLibrary {
codeOutput = append(codeOutput, generatedLine("; SCRIPT LIBRARY")) codeOutput = append(codeOutput, "; SCRIPT LIBRARY")
scriptIsLibrary = true scriptIsLibrary = true
} else if line.Kind == preproc.ScriptMacroDef { } else if line.Kind == preproc.ScriptMacroDef {
// First line is the header - parse it // First line is the header - parse it
@ -173,7 +124,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} }
currentMacroName = name currentMacroName = name
currentMacroParams = params currentMacroParams = params
codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; %s", line.Text))) codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
} }
lastKind = line.Kind lastKind = line.Kind
@ -187,7 +138,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
c.printErrorWithContext(lines, i, fmt.Errorf("internal error: ASM line without active ASM block")) c.printErrorWithContext(lines, i, fmt.Errorf("internal error: ASM line without active ASM block"))
return nil, fmt.Errorf("compilation failed") return nil, fmt.Errorf("compilation failed")
} }
text := line.Text text := line.Text
// Find comment boundary - only process |...| patterns in the code portion // Find comment boundary - only process |...| patterns in the code portion
@ -212,16 +163,16 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
return nil, fmt.Errorf("compilation failed") return nil, fmt.Errorf("compilation failed")
} }
macroOutput, err := ExecuteMacro(macroName, args, c.ctx, line.PragmaSetIndex) macroOutput, err := ExecuteMacro(macroName, args, c.ctx)
if err != nil { if err != nil {
c.printErrorWithContext(lines, i, fmt.Errorf("macro %s: %w", macroName, err)) c.printErrorWithContext(lines, i, fmt.Errorf("macro %s: %w", macroName, err))
return nil, fmt.Errorf("compilation failed") return nil, fmt.Errorf("compilation failed")
} }
// Emit with comments showing invocation // Emit with comments showing invocation
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine(fmt.Sprintf("; %s", text))) *currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; %s", text))
*currentAsmTarget = append(*currentAsmTarget, asmSourceLines(macroOutput)...) *currentAsmTarget = append(*currentAsmTarget, macroOutput...)
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine(fmt.Sprintf("; end @%s", macroName))) *currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; end @%s", macroName))
continue continue
} }
} }
@ -240,30 +191,20 @@ 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
searchFrom = start + len(expandedName) searchFrom = start + len(expandedName)
} }
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine(codePart+commentPart)) *currentAsmTarget = append(*currentAsmTarget, codePart+commentPart)
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary { } else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
// Collect script lines for execution // Collect script lines for execution
scriptBuffer = append(scriptBuffer, line) scriptBuffer = append(scriptBuffer, line.Text)
} else if line.Kind == preproc.ScriptMacroDef { } else if line.Kind == preproc.ScriptMacroDef {
// Skip the header line (already parsed in transition) // Skip the header line (already parsed in transition)
if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") { if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") {
continue continue
} }
// Capture source provenance from first body line
if len(macroBuffer) == 0 {
currentMacroSourceFile = line.Filename
currentMacroStartLine = line.LineNo
}
// Collect macro body lines // Collect macro body lines
macroBuffer = append(macroBuffer, line.Text) macroBuffer = append(macroBuffer, line.Text)
} }
@ -295,18 +236,18 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
return nil, fmt.Errorf("compilation failed") return nil, fmt.Errorf("compilation failed")
} }
codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; %s", line.Text))) codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
if len(asmLines) > 0 && c.isMarkersEnabled() { if len(asmLines) > 0 && c.isMarkersEnabled() {
codeOutput = append(codeOutput, generatedLine(fmt.Sprintf("; @@OPT:%s:%s", classString(cmd.GetClass()), cmd.GetName()))) codeOutput = append(codeOutput, fmt.Sprintf("; @@OPT:%s:%s", classString(cmd.GetClass()), cmd.GetName()))
} }
codeOutput = append(codeOutput, generatedLines(asmLines)...) codeOutput = append(codeOutput, asmLines...)
} }
// Close any open block // Close any open block
if lastKind == preproc.Assembler { if lastKind == preproc.Assembler {
// Close the final ASM block if still open // Close the final ASM block if still open
if currentAsmTarget != nil { if currentAsmTarget != nil {
*currentAsmTarget = append(*currentAsmTarget, asmSourceLine("; ENDASM")) *currentAsmTarget = append(*currentAsmTarget, "; ENDASM")
} }
return nil, fmt.Errorf("Unclosed ASM block.") return nil, fmt.Errorf("Unclosed ASM block.")
} else if lastKind == preproc.Script { } else if lastKind == preproc.Script {
@ -318,13 +259,8 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} }
// Peephole optimization pass // Peephole optimization pass
var codeStrings []string
if cfg := c.getOptimizerConfig(); cfg != nil { if cfg := c.getOptimizerConfig(); cfg != nil {
var dissolved map[string]bool codeOutput = optimizer.Optimize(codeOutput, cfg)
codeStrings, dissolved = optimizer.Optimize(codeOutput, cfg)
c.dissolvedVars = dissolved
} else {
codeStrings = optimizer.SourceLineTexts(codeOutput)
} }
// Analyze for overlapping absolute addresses in function call chains // Analyze for overlapping absolute addresses in function call chains
@ -332,9 +268,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Get functions with _P_REMOVE_UNUSED pragma (for suppressing variable warnings) // Get functions with _P_REMOVE_UNUSED pragma (for suppressing variable warnings)
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, c.dissolvedVars) warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma)
for _, warning := range warnings { for _, warning := range warnings {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning) _, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
} }
@ -346,13 +282,13 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} }
// Remove unused functions with _P_REMOVE_UNUSED pragma // Remove unused functions with _P_REMOVE_UNUSED pragma
codeStrings, removedFuncs := c.removeUnusedFunctions(codeStrings) codeOutput, removedFuncs := c.removeUnusedFunctions(codeOutput)
// Update peephole header to match actual [removed] count after function removal // Update peephole header to match actual [removed] count after function removal
codeStrings = updatePeepholeHeader(codeStrings) codeOutput = updatePeepholeHeader(codeOutput)
// Assemble final output with headers and footers // Assemble final output with headers and footers
return c.assembleOutput(codeStrings, removedFuncs), nil return c.assembleOutput(codeOutput, removedFuncs), nil
} }
// isOptimizing returns true if any peephole optimization pragma is active // isOptimizing returns true if any peephole optimization pragma is active
@ -395,20 +331,11 @@ 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
} }
@ -786,7 +713,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, c.dissolvedVars); len(varLines) > 0 { if varLines := GenerateVariables(c.ctx.SymbolTable, removedFuncs); len(varLines) > 0 {
output = append(output, varLines...) output = append(output, varLines...)
} }
@ -802,7 +729,7 @@ func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bo
if len(c.deferredAsm) > 0 { if len(c.deferredAsm) > 0 {
output = append(output, "; Deferred ASM blocks (after variables)") output = append(output, "; Deferred ASM blocks (after variables)")
output = append(output, "") output = append(output, "")
output = append(output, optimizer.SourceLineTexts(c.deferredAsm)...) output = append(output, c.deferredAsm...)
output = append(output, "") output = append(output, "")
} }

View file

@ -165,7 +165,7 @@ func TestExecuteScript_BasicPrint(t *testing.T) {
" print(' nop')", " print(' nop')",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("executeScript failed: %v", err) t.Fatalf("executeScript failed: %v", err)
} }
@ -189,7 +189,7 @@ func TestExecuteScript_EmptyOutput(t *testing.T) {
"x = 1 + 1", "x = 1 + 1",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("executeScript failed: %v", err) t.Fatalf("executeScript failed: %v", err)
} }
@ -210,7 +210,7 @@ func TestExecuteScript_Library_DefinesFunction(t *testing.T) {
" print(' nop')", " print(' nop')",
} }
_, err := testExecuteScript(libraryLines, ctx, true) _, err := executeScript(libraryLines, ctx, true)
if err != nil { if err != nil {
t.Fatalf("library executeScript failed: %v", err) t.Fatalf("library executeScript failed: %v", err)
} }
@ -232,7 +232,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
" print(' nop')", " print(' nop')",
} }
_, err := testExecuteScript(libraryLines, ctx, true) _, err := executeScript(libraryLines, ctx, true)
if err != nil { if err != nil {
t.Fatalf("library executeScript failed: %v", err) t.Fatalf("library executeScript failed: %v", err)
} }
@ -242,7 +242,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
"emit_nops(2)", "emit_nops(2)",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("script executeScript failed: %v", err) t.Fatalf("script executeScript failed: %v", err)
} }
@ -267,7 +267,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
"def func_a():", "def func_a():",
" print(' ; from a')", " print(' ; from a')",
} }
_, err := testExecuteScript(lib1, ctx, true) _, err := executeScript(lib1, ctx, true)
if err != nil { if err != nil {
t.Fatalf("lib1 failed: %v", err) t.Fatalf("lib1 failed: %v", err)
} }
@ -277,7 +277,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
"def func_b():", "def func_b():",
" print(' ; from b')", " print(' ; from b')",
} }
_, err = testExecuteScript(lib2, ctx, true) _, err = executeScript(lib2, ctx, true)
if err != nil { if err != nil {
t.Fatalf("lib2 failed: %v", err) t.Fatalf("lib2 failed: %v", err)
} }
@ -295,7 +295,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
"func_a()", "func_a()",
"func_b()", "func_b()",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("script failed: %v", err) t.Fatalf("script failed: %v", err)
} }
@ -322,7 +322,7 @@ func TestExecuteScript_RegularScript_DoesNotPersist(t *testing.T) {
"local_func()", "local_func()",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("script failed: %v", err) t.Fatalf("script failed: %v", err)
} }
@ -352,7 +352,7 @@ func TestExecuteMacro_Basic(t *testing.T) {
} }
// Execute macro // Execute macro
output, err := ExecuteMacro("test_macro", []string{"3"}, ctx, 0) output, err := ExecuteMacro("test_macro", []string{"3"}, ctx)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -377,7 +377,7 @@ func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
"def emit_nop():", "def emit_nop():",
" print(' nop')", " print(' nop')",
} }
_, err := testExecuteScript(lib, ctx, true) _, err := executeScript(lib, ctx, true)
if err != nil { if err != nil {
t.Fatalf("library failed: %v", err) t.Fatalf("library failed: %v", err)
} }
@ -392,7 +392,7 @@ func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
} }
// Execute macro // Execute macro
output, err := ExecuteMacro("nop_macro", []string{}, ctx, 0) output, err := ExecuteMacro("nop_macro", []string{}, ctx)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -416,7 +416,7 @@ func TestExecuteMacro_StringParameter(t *testing.T) {
} }
// Execute with identifier (should be passed as string) // Execute with identifier (should be passed as string)
output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx, 0) output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -451,7 +451,7 @@ func TestExecuteMacro_LocalVariableExpansion(t *testing.T) {
} }
// Execute macro with "myvar" as argument - should expand |myvar| to testfunc_myvar // Execute macro with "myvar" as argument - should expand |myvar| to testfunc_myvar
output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx, 0) output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -500,7 +500,7 @@ func TestExecuteMacro_LocalVariableExpansion_MultipleVars(t *testing.T) {
} }
// Execute macro with actual variable names as arguments // Execute macro with actual variable names as arguments
output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx, 0) output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -540,7 +540,7 @@ func TestExecuteScript_LocalVariableExpansion(t *testing.T) {
"print(' inc |counter|')", "print(' inc |counter|')",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("executeScript failed: %v", err) t.Fatalf("executeScript failed: %v", err)
} }
@ -569,7 +569,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
" print(' inc |global_counter|')", " print(' inc |global_counter|')",
} }
_, err := testExecuteScript(libraryLines, ctx, true) _, err := executeScript(libraryLines, ctx, true)
if err != nil { if err != nil {
t.Fatalf("library script failed: %v", err) t.Fatalf("library script failed: %v", err)
} }
@ -579,7 +579,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
"inc_global()", "inc_global()",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("executeScript failed: %v", err) t.Fatalf("executeScript failed: %v", err)
} }
@ -610,7 +610,7 @@ func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
} }
// Library defined at global scope - |local_var| won't find caller's local // Library defined at global scope - |local_var| won't find caller's local
_, err := testExecuteScript(libraryLines, ctx, true) _, err := executeScript(libraryLines, ctx, true)
if err != nil { if err != nil {
t.Fatalf("library script failed: %v", err) t.Fatalf("library script failed: %v", err)
} }
@ -625,7 +625,7 @@ func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
"use_local()", "use_local()",
} }
output, err := testExecuteScript(scriptLines, ctx, false) output, err := executeScript(scriptLines, ctx, false)
if err != nil { if err != nil {
t.Fatalf("executeScript failed: %v", err) t.Fatalf("executeScript failed: %v", err)
} }
@ -1219,96 +1219,3 @@ func TestAsmAfterVarsWithVariables(t *testing.T) {
t.Errorf("expected ASM block in deferred section") t.Errorf("expected ASM block in deferred section")
} }
} }
// testExecuteScript is a test helper that wraps executeScript with convenient types
func testExecuteScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
lines := make([]preproc.Line, len(scriptLines))
for i, text := range scriptLines {
lines[i] = preproc.Line{
Text: text,
Filename: "test.c65",
LineNo: i + 1,
}
}
return executeScript(lines, ctx, isLibrary)
}
func TestCompile_MultipleScriptBlocks_Success(t *testing.T) {
// Two consecutive SCRIPT blocks should execute independently
pragma := preproc.NewPragma()
comp := NewCompiler(pragma)
lines := []preproc.Line{
// Block 1: print("hello")
{Text: "print('hello')", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
// ENDSCRIPT boundary (empty Source)
{Text: "", Filename: "test.c65", LineNo: 3, Kind: preproc.Source},
// Block 2: print("world")
{Text: "print('world')", Filename: "test.c65", LineNo: 5, Kind: preproc.Script},
// ENDSCRIPT boundary (empty Source)
{Text: "", Filename: "test.c65", LineNo: 6, Kind: preproc.Source},
}
output, err := comp.Compile(lines)
if err != nil {
t.Fatalf("Compile failed: %v", err)
}
// Output should contain both "hello" and "world" in that order
foundHello := false
foundWorld := false
helloBeforeWorld := false
for _, line := range output {
if strings.Contains(line, "hello") && !foundHello {
foundHello = true
}
if foundHello && strings.Contains(line, "world") && !foundWorld {
foundWorld = true
helloBeforeWorld = true
}
}
if !foundHello {
t.Error("expected 'hello' from block 1 in output")
}
if !foundWorld {
t.Error("expected 'world' from block 2 in output")
}
if !helloBeforeWorld {
t.Error("expected 'hello' before 'world' in output")
}
}
func TestCompile_MultipleScriptBlocks_ErrorInBlock2(t *testing.T) {
// Error in second SCRIPT block should not be confused with block 1
pragma := preproc.NewPragma()
comp := NewCompiler(pragma)
lines := []preproc.Line{
// Block 1: no error
{Text: "x = 1", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
{Text: "print(x)", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
// ENDSCRIPT boundary
{Text: "", Filename: "test.c65", LineNo: 4, Kind: preproc.Source},
// Block 2: error at line 8 (division by zero)
{Text: "y = 2", Filename: "test.c65", LineNo: 6, Kind: preproc.Script},
{Text: "z = y + 1", Filename: "test.c65", LineNo: 7, Kind: preproc.Script},
{Text: "1 / 0", Filename: "test.c65", LineNo: 8, Kind: preproc.Script},
// ENDSCRIPT boundary
{Text: "", Filename: "test.c65", LineNo: 9, Kind: preproc.Source},
}
_, err := comp.Compile(lines)
if err == nil {
t.Fatal("expected error from block 2, got none")
}
errMsg := err.Error()
if !strings.Contains(errMsg, "Starlark error") {
t.Errorf("expected Starlark error, got: %s", errMsg)
}
// Verify error references the correct source line in block 2 (line 8 = division by zero)
if !strings.Contains(errMsg, ":8:") {
t.Errorf("error should reference source line 8 in block 2, got: %s", errMsg)
}
}

View file

@ -8,11 +8,9 @@ import (
// ScriptMacro represents a named, parameterized script macro // ScriptMacro represents a named, parameterized script macro
type ScriptMacro struct { type ScriptMacro struct {
Name string // macro name Name string // macro name
Params []string // parameter names Params []string // parameter names
Body []string // Starlark code lines (the macro body) Body []string // Starlark code lines (the macro body)
SourceFile string // source file where macro is defined
StartLine int // 1-based line number in source file of first body line
} }
// CompilerContext holds all shared resources needed by commands during compilation // CompilerContext holds all shared resources needed by commands during compilation
@ -43,11 +41,6 @@ type CompilerContext struct {
// ScriptMacros holds named macro definitions from SCRIPT MACRO blocks // ScriptMacros holds named macro definitions from SCRIPT MACRO blocks
ScriptMacros map[string]*ScriptMacro ScriptMacros map[string]*ScriptMacro
// ProjectRoot is the absolute path of the directory containing the main input .c65 file.
// Used by scripting built-ins (load_binary, load_text) to resolve relative file paths
// and enforce security (no access outside project root).
ProjectRoot string
} }
// NewCompilerContext creates a new compiler context with initialized resources // NewCompilerContext creates a new compiler context with initialized resources

View file

@ -52,7 +52,7 @@ type FunctionHandler struct {
// Absolute address tracking for overlap detection // Absolute address tracking for overlap detection
absoluteAddrs map[string]map[uint16]bool // funcName -> set of absolute addresses used absoluteAddrs map[string]map[uint16]bool // funcName -> set of absolute addresses used
callGraph map[string][]string // funcName -> list of functions it calls callGraph map[string][]string // funcName -> list of functions it calls
// Function usage tracking for unused function warnings // Function usage tracking for unused function warnings
calledFunctions map[string]bool // funcName -> true if function is called calledFunctions map[string]bool // funcName -> true if function is called
@ -159,11 +159,6 @@ 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,
@ -403,7 +398,7 @@ func (fh *FunctionHandler) HandleFuncCall(line preproc.Line) ([]string, error) {
// Generate final assembly // Generate final assembly
asmLines = append(asmLines, inAssigns...) asmLines = append(asmLines, inAssigns...)
asmLines = append(asmLines, fmt.Sprintf("\tjsr %s", funcName)) asmLines = append(asmLines, fmt.Sprintf(" jsr %s", funcName))
asmLines = append(asmLines, outAssigns...) asmLines = append(asmLines, outAssigns...)
return asmLines, nil return asmLines, nil
@ -451,10 +446,10 @@ func (fh *FunctionHandler) processLabelArg(arg string, param *FuncParam, funcNam
} }
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf("\tlda #<%s", labelName), fmt.Sprintf(" lda #<%s", labelName),
fmt.Sprintf("\tsta %s", param.Symbol.FullName()), fmt.Sprintf(" sta %s", param.Symbol.FullName()),
fmt.Sprintf("\tlda #>%s", labelName), fmt.Sprintf(" lda #>%s", labelName),
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()), fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
) )
return nil return nil
@ -475,10 +470,10 @@ func (fh *FunctionHandler) processStringArg(arg string, param *FuncParam, funcNa
actualLabel := fh.constStrHandler.AddConstStr(labelName, arg, true, pragmaSet) actualLabel := fh.constStrHandler.AddConstStr(labelName, arg, true, pragmaSet)
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf("\tlda #<%s", actualLabel), fmt.Sprintf(" lda #<%s", actualLabel),
fmt.Sprintf("\tsta %s", param.Symbol.FullName()), fmt.Sprintf(" sta %s", param.Symbol.FullName()),
fmt.Sprintf("\tlda #>%s", actualLabel), fmt.Sprintf(" lda #>%s", actualLabel),
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()), fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
) )
return nil return nil
@ -492,20 +487,20 @@ func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName
// Generate IN assignments (sym -> param) // Generate IN assignments (sym -> param)
if param.Direction.Has(DirIn) { if param.Direction.Has(DirIn) {
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf("\tlda %s", sym.FullName()), fmt.Sprintf(" lda %s", sym.FullName()),
fmt.Sprintf("\tsta %s", param.Symbol.FullName()), fmt.Sprintf(" sta %s", param.Symbol.FullName()),
) )
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
if sym.IsWord() { if sym.IsWord() {
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf("\tlda %s+1", sym.FullName()), fmt.Sprintf(" lda %s+1", sym.FullName()),
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()), fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
) )
} else { } else {
// byte -> word: zero extend // byte -> word: zero extend
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
"\tlda #0", " lda #0",
fmt.Sprintf("\tsta %s+1", param.Symbol.FullName()), fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
) )
} }
} else if sym.IsWord() { } else if sym.IsWord() {
@ -518,20 +513,20 @@ func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName
// Generate OUT assignments (param -> sym) // Generate OUT assignments (param -> sym)
if param.Direction.Has(DirOut) { if param.Direction.Has(DirOut) {
*outAssigns = append(*outAssigns, *outAssigns = append(*outAssigns,
fmt.Sprintf("\tlda %s", param.Symbol.FullName()), fmt.Sprintf(" lda %s", param.Symbol.FullName()),
fmt.Sprintf("\tsta %s", sym.FullName()), fmt.Sprintf(" sta %s", sym.FullName()),
) )
if sym.IsWord() { if sym.IsWord() {
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
*outAssigns = append(*outAssigns, *outAssigns = append(*outAssigns,
fmt.Sprintf("\tlda %s+1", param.Symbol.FullName()), fmt.Sprintf(" lda %s+1", param.Symbol.FullName()),
fmt.Sprintf("\tsta %s+1", sym.FullName()), fmt.Sprintf(" sta %s+1", sym.FullName()),
) )
} else { } else {
// byte -> word: zero extend // byte -> word: zero extend
*outAssigns = append(*outAssigns, *outAssigns = append(*outAssigns,
"\tlda #0", " lda #0",
fmt.Sprintf("\tsta %s+1", sym.FullName()), fmt.Sprintf(" sta %s+1", sym.FullName()),
) )
} }
} else if param.Symbol.IsWord() { } else if param.Symbol.IsWord() {
@ -576,16 +571,16 @@ func (fh *FunctionHandler) processConstArg(arg string, param *FuncParam, funcNam
highByte := uint8((value >> 8) & 0xFF) highByte := uint8((value >> 8) & 0xFF)
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf("\tlda #%d", lowByte), fmt.Sprintf(" lda #%d", lowByte),
fmt.Sprintf("\tsta %s", param.Symbol.FullName()), fmt.Sprintf(" sta %s", param.Symbol.FullName()),
) )
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
// Optimize: only reload A if high byte differs // Optimize: only reload A if high byte differs
if highByte != lowByte { if highByte != lowByte {
*inAssigns = append(*inAssigns, fmt.Sprintf("\tlda #%d", highByte)) *inAssigns = append(*inAssigns, fmt.Sprintf(" lda #%d", highByte))
} }
*inAssigns = append(*inAssigns, fmt.Sprintf("\tsta %s+1", param.Symbol.FullName())) *inAssigns = append(*inAssigns, fmt.Sprintf(" sta %s+1", param.Symbol.FullName()))
} }
return nil return nil
@ -604,41 +599,30 @@ func (fh *FunctionHandler) processConstValue(value uint16, param *FuncParam, fun
highByte := uint8((value >> 8) & 0xFF) highByte := uint8((value >> 8) & 0xFF)
*inAssigns = append(*inAssigns, *inAssigns = append(*inAssigns,
fmt.Sprintf("\tlda #%d", lowByte), fmt.Sprintf(" lda #%d", lowByte),
fmt.Sprintf("\tsta %s", param.Symbol.FullName()), fmt.Sprintf(" sta %s", param.Symbol.FullName()),
) )
if param.Symbol.IsWord() { if param.Symbol.IsWord() {
// Optimize: only reload A if high byte differs // Optimize: only reload A if high byte differs
if highByte != lowByte { if highByte != lowByte {
*inAssigns = append(*inAssigns, fmt.Sprintf("\tlda #%d", highByte)) *inAssigns = append(*inAssigns, fmt.Sprintf(" lda #%d", highByte))
} }
*inAssigns = append(*inAssigns, fmt.Sprintf("\tsta %s+1", param.Symbol.FullName())) *inAssigns = append(*inAssigns, fmt.Sprintf(" sta %s+1", param.Symbol.FullName()))
} }
return nil return nil
} }
// parseImplicitDecl parses {BYTE varname} or {WORD varname} or {BYTE REGISTER varname} or {BYTE varname @ address} and adds to symbol table // parseImplicitDecl parses {BYTE varname} or {WORD 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', 'TYPE REGISTER name', or 'TYPE name @ addr', got: %q", decl) return fmt.Errorf("implicit declaration must be 'TYPE name' or 'TYPE name @ addr', got: %q", decl)
} }
typeIdx := 0
typeStr := strings.ToUpper(parts[0]) typeStr := strings.ToUpper(parts[0])
register := false varName := parts[1]
// 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 {
@ -650,48 +634,33 @@ 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 register && kind != KindByte { if len(parts) == 2 {
return fmt.Errorf("REGISTER hint is only valid for BYTE variables") // Simple: BYTE name or WORD name
}
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: TYPE [REGISTER] name @ address // Extended: BYTE name @ address or WORD name @ address
if len(parts) == 4+typeIdx { operator := parts[2]
operator := parts[2+typeIdx] addrStr := parts[3]
addrStr := parts[3+typeIdx]
if register { if operator != "@" {
return fmt.Errorf("REGISTER variable cannot be @-mapped; REGISTER is incompatible with fixed address") return fmt.Errorf("expected '@' operator, got: %q", operator)
}
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)
} }
return fmt.Errorf("invalid implicit declaration format: %q", decl) // 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)
} }
// EndFunction pops all functions from the stack (called by FEND) // EndFunction pops all functions from the stack (called by FEND)
@ -894,7 +863,7 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
} }
} }
// Check for implicit declaration {TYPE name} or {TYPE REGISTER name} // Check for implicit declaration {TYPE 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 { }
@ -904,12 +873,7 @@ 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] varName = parts[1]
if len(parts) >= 3 && strings.ToUpper(parts[1]) == "REGISTER" {
varName = parts[2]
} else {
varName = parts[1]
}
} }
return direction, varName, isImplicit, implicitDecl, nil return direction, varName, isImplicit, implicitDecl, nil
@ -917,10 +881,10 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
// AbsoluteOverlap represents a detected overlap in absolute addresses // AbsoluteOverlap represents a detected overlap in absolute addresses
type AbsoluteOverlap struct { type AbsoluteOverlap struct {
Func1 string // First function using the address Func1 string // First function using the address
Func2 string // Second function using the address Func2 string // Second function using the address
Address uint16 // Overlapping address Address uint16 // Overlapping address
CallChain []string // Call chain from Func1 to Func2 CallChain []string // Call chain from Func1 to Func2
} }
// AnalyzeAbsoluteOverlaps checks for overlapping absolute addresses in call chains // AnalyzeAbsoluteOverlaps checks for overlapping absolute addresses in call chains

View file

@ -399,13 +399,13 @@ func TestHandleFuncCall_VarArgs(t *testing.T) {
// Check generated assembly // Check generated assembly
expectedLines := []string{ expectedLines := []string{
"\tlda var_a", " lda var_a",
"\tsta test_func_param_a", " sta test_func_param_a",
"\tlda var_b", " lda var_b",
"\tsta test_func_param_b", " sta test_func_param_b",
"\tlda var_b+1", " lda var_b+1",
"\tsta test_func_param_b+1", " sta test_func_param_b+1",
"\tjsr test_func", " jsr test_func",
} }
if len(asm) != len(expectedLines) { if len(asm) != len(expectedLines) {
@ -928,13 +928,13 @@ func TestHandleFuncCall_AbsoluteParams(t *testing.T) {
// Check generated assembly uses correct names // Check generated assembly uses correct names
expectedLines := []string{ expectedLines := []string{
"\tlda var_a", " lda var_a",
"\tsta test_abs_param_a", " sta test_abs_param_a",
"\tlda var_b", " lda var_b",
"\tsta test_abs_param_b", " sta test_abs_param_b",
"\tlda var_b+1", " lda var_b+1",
"\tsta test_abs_param_b+1", " sta test_abs_param_b+1",
"\tjsr test_abs", " jsr test_abs",
} }
if len(asm) != len(expectedLines) { if len(asm) != len(expectedLines) {
@ -1958,91 +1958,3 @@ 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

@ -2,232 +2,29 @@ package compiler
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings" "strings"
"c65gm/internal/preproc"
"c65gm/internal/utils" "c65gm/internal/utils"
"go.starlark.net/lib/math" "go.starlark.net/lib/math"
"go.starlark.net/starlark" "go.starlark.net/starlark"
) )
// mapStarlarkLine maps a Starlark 1-based line number to an index into scriptLines
// (a slice of preproc.Line). Returns -1 if the line cannot be mapped.
// For non-library scripts, the Starlark source is:
//
// Line 1: def _main():
// Line 2..N+1: indented script lines
// Line N+2: _main()
//
// For library scripts, lines are used as-is.
func mapStarlarkLine(starlarkLine int, numScriptLines int, isLibrary bool) int {
var idx int
if isLibrary {
idx = starlarkLine - 1
} else {
idx = starlarkLine - 2
}
if idx < 0 || idx >= numScriptLines {
return -1
}
return idx
}
// starlarkErrorMsgLineMatch matches Starlark error messages of the form
// "filename:LINE:COL: message" produced by resolve errors.
var starlarkErrorMsgLineMatch = regexp.MustCompile(`^(.+):(\d+):(\d+): (.*)$`)
// starlarkPosition extracts the source position from a Starlark error.
// Returns the 1-based line number (0 if unknown).
func starlarkPosition(err error) int {
var evalErr *starlark.EvalError
if errors.As(err, &evalErr) {
if len(evalErr.CallStack) > 0 {
top := evalErr.CallStack.At(0)
return int(top.Pos.Line)
}
}
// For non-EvalError (resolve errors), extract line from the message
return parseStarlarkLineFromMsg(err.Error())
}
// parseStarlarkLineFromMsg attempts to extract the Starlark line number from
// a non-EvalError error message string. Starlark resolve errors follow the
// format "filename:LINE:COL: message".
func parseStarlarkLineFromMsg(msg string) int {
matches := starlarkErrorMsgLineMatch.FindStringSubmatch(msg)
if len(matches) >= 3 {
line, err := strconv.Atoi(matches[2])
if err == nil && line > 0 {
return line
}
}
return 0
}
// starlarkErrorMsg extracts just the message from a Starlark error.
// For EvalError, it returns the Msg field directly.
// For non-EvalError (resolve errors), it strips the "filename:LINE:COL: " prefix.
func starlarkErrorMsg(err error) string {
var evalErr *starlark.EvalError
if errors.As(err, &evalErr) {
return evalErr.Msg
}
return stripStarlarkPositionFromMsg(err.Error())
}
// stripStarlarkPositionFromMsg removes the "filename:LINE:COL: " prefix from
// a Starlark error message if present.
func stripStarlarkPositionFromMsg(msg string) string {
matches := starlarkErrorMsgLineMatch.FindStringSubmatch(msg)
if len(matches) >= 5 {
return matches[4]
}
return msg
}
// printScriptErrorContext prints a Starlark error with script-block-bounded source context,
// synthesizing SCRIPT/ENDSCRIPT boundary lines that the preprocessor discards.
// When the original Starlark error has a call stack with frames beyond the script block
// (e.g. a library function), those frames are shown as a backtrace below the source context.
func printScriptErrorContext(err error, errMsg string, scriptLines []preproc.Line, errorIdx int, blockType string) {
if len(scriptLines) == 0 || errorIdx < 0 || errorIdx >= len(scriptLines) {
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n\n", errMsg)
return
}
line := scriptLines[errorIdx]
filename := line.Filename
const contextLines = 3
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n", errMsg)
fmt.Fprintf(os.Stderr, " --> %s:%d\n\n", filename, line.LineNo)
startIdx := errorIdx - contextLines
if startIdx < 0 {
startIdx = 0
}
endIdx := errorIdx + contextLines
if endIdx >= len(scriptLines) {
endIdx = len(scriptLines) - 1
}
scriptMarkerLineNo := scriptLines[0].LineNo - 1
endScriptLineNo := scriptLines[len(scriptLines)-1].LineNo + 1
maxLineNo := endScriptLineNo
if scriptLines[endIdx].LineNo > maxLineNo {
maxLineNo = scriptLines[endIdx].LineNo
}
if scriptMarkerLineNo > maxLineNo {
maxLineNo = scriptMarkerLineNo
}
lineNumWidth := len(fmt.Sprintf("%d", maxLineNo))
if startIdx == 0 {
fmt.Fprintf(os.Stderr, " %*d | %s\n", lineNumWidth, scriptMarkerLineNo, blockType)
}
for i := startIdx; i <= endIdx; i++ {
l := scriptLines[i]
marker := " "
if i == errorIdx {
marker = ">> "
}
fmt.Fprintf(os.Stderr, "%s%*d | %s\n", marker, lineNumWidth, l.LineNo, l.Text)
}
if endIdx == len(scriptLines)-1 {
fmt.Fprintf(os.Stderr, " %*d | END%s\n", lineNumWidth, endScriptLineNo, blockType)
}
// Show call stack if the error has additional frames beyond the script block
printStarlarkBacktrace(err, line.LineNo, filename)
fmt.Fprintf(os.Stderr, "\n")
}
// printStarlarkBacktrace prints a concise call stack from a Starlark error,
// filtering out frames from the given scriptBlockLine and <toplevel>/_main wrappers.
func printStarlarkBacktrace(err error, scriptBlockLine int, scriptFile string) {
var evalErr *starlark.EvalError
if !errors.As(err, &evalErr) || len(evalErr.CallStack) < 2 {
return
}
var frames []string
for _, cf := range evalErr.CallStack {
name := cf.Name
pos := cf.Pos
// Skip frames that are already shown in the bounded source context
if name == "<toplevel>" || name == "_main" {
continue
}
if int(pos.Line) == scriptBlockLine && pos.Filename() == scriptFile {
continue
}
if pos.Filename() != "" && int(pos.Line) > 0 {
frames = append(frames, fmt.Sprintf(" %s: in %s", pos.String(), name))
}
}
if len(frames) > 0 {
fmt.Fprintf(os.Stderr, "Call stack:\n%s\n", strings.Join(frames, "\n"))
}
}
// printScriptErrorFallback prints what we can from a Starlark error when
// we couldn't map it to a specific source line within a script block.
func printScriptErrorFallback(err error) {
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n", err)
var evalErr *starlark.EvalError
if errors.As(err, &evalErr) && len(evalErr.CallStack) > 0 {
fmt.Fprintf(os.Stderr, "Call stack:\n")
for _, cf := range evalErr.CallStack {
name := cf.Name
pos := cf.Pos
if pos.Filename() != "" && int(pos.Line) > 0 {
fmt.Fprintf(os.Stderr, " %s: in %s\n", pos.String(), name)
}
}
}
fmt.Fprintf(os.Stderr, "\n")
}
// executeScript runs a Starlark script and returns the output lines. // executeScript runs a Starlark script and returns the output lines.
// If isLibrary is true, the script is executed at top level (no _main wrapper) // If isLibrary is true, the script is executed at top level (no _main wrapper)
// and resulting globals are persisted to ctx.ScriptLibraryGlobals. // and resulting globals are persisted to ctx.ScriptLibraryGlobals.
func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary bool) ([]string, error) { func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
// Extract text from preproc.Lines
texts := make([]string, len(scriptLines))
for i, l := range scriptLines {
texts[i] = l.Text
}
// Join script lines // Join script lines
scriptText := strings.Join(texts, "\n") scriptText := strings.Join(scriptLines, "\n")
// Expand |varname| -> actual variable names // Expand |varname| -> actual variable names
scriptText, err := expandVariables(scriptText, ctx) scriptText = expandVariables(scriptText, ctx)
if err != nil {
return nil, err
}
// Determine the source filename for Starlark
sourceFile := scriptLines[0].Filename
var finalScript string var finalScript string
var starlarkFilename string
if isLibrary { if isLibrary {
// LIBRARY: execute at top level so defs become globals // LIBRARY: execute at top level so defs become globals
finalScript = scriptText finalScript = scriptText
starlarkFilename = sourceFile
} else { } else {
// Regular SCRIPT: wrap in function (Starlark requires control flow inside functions) // Regular SCRIPT: wrap in function (Starlark requires control flow inside functions)
finalScript = "def _main():\n" finalScript = "def _main():\n"
@ -235,7 +32,6 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
finalScript += " " + line + "\n" finalScript += " " + line + "\n"
} }
finalScript += "_main()\n" finalScript += "_main()\n"
starlarkFilename = sourceFile
} }
// Capture print output // Capture print output
@ -247,41 +43,21 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
}, },
} }
// Set execution limit from pragma or default (prevent infinite loops) // Set execution limit (prevent infinite loops)
thread.SetMaxExecutionSteps(readScriptMaxSteps(ctx, scriptLines[0].PragmaSetIndex)) thread.SetMaxExecutionSteps(1000000) // 1M steps
// Build predeclared: math module + library globals + file I/O builtins // Build predeclared: math module + library globals
predeclared := starlark.StringDict{ predeclared := starlark.StringDict{
"math": math.Module, "math": math.Module,
"load_binary": makeLoadBinary(ctx.ProjectRoot),
"load_text": makeLoadText(ctx.ProjectRoot),
} }
for k, v := range ctx.ScriptLibraryGlobals { for k, v := range ctx.ScriptLibraryGlobals {
predeclared[k] = v predeclared[k] = v
} }
// Execute // Execute
globals, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared) globals, err := starlark.ExecFile(thread, "script.star", finalScript, predeclared)
if err != nil { if err != nil {
// Map Starlark error position back to source return nil, err
starLine := starlarkPosition(err)
idx := -1
if starLine > 0 {
idx = mapStarlarkLine(starLine, len(scriptLines), isLibrary)
}
if idx >= 0 {
msg := starlarkErrorMsg(err)
blockType := "SCRIPT"
if isLibrary {
blockType = "SCRIPT LIBRARY"
}
printScriptErrorContext(err, msg, scriptLines, idx, blockType)
srcLine := scriptLines[idx]
return nil, fmt.Errorf("Starlark error: %s:%d: %s", srcLine.Filename, srcLine.LineNo, msg)
}
// Fallback: print whatever info we can extract
printScriptErrorFallback(err)
return nil, fmt.Errorf("Starlark error: %w", err)
} }
// For LIBRARY: persist new globals (functions, variables defined at top level) // For LIBRARY: persist new globals (functions, variables defined at top level)
@ -301,7 +77,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, error) { func expandVariables(text string, ctx *CompilerContext) string {
result := text result := text
for { for {
start := strings.IndexByte(result, '|') start := strings.IndexByte(result, '|')
@ -315,159 +91,14 @@ func expandVariables(text string, ctx *CompilerContext) (string, error) {
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, nil return result
}
// validateScriptFilePath checks that path is safe and resolves it within the project root.
// It rejects absolute paths and path traversal (..).
func validateScriptFilePath(projectRoot, path string) (string, error) {
if projectRoot == "" {
return "", fmt.Errorf("project root not set (internal error)")
}
if path == "" {
return "", fmt.Errorf("file path must not be empty")
}
if filepath.IsAbs(path) {
return "", fmt.Errorf("absolute paths are not allowed: %s", path)
}
// Reject path traversal components
cleaned := filepath.Clean(path)
for _, component := range strings.Split(cleaned, string(filepath.Separator)) {
if component == ".." {
return "", fmt.Errorf("path traversal is not allowed: %s", path)
}
}
// Resolve against project root
resolved := filepath.Join(projectRoot, cleaned)
// Verify containment within project root
projectRootWithSep := projectRoot + string(filepath.Separator)
if !strings.HasPrefix(resolved, projectRootWithSep) && resolved != projectRoot {
return "", fmt.Errorf("file access denied: path resolves outside project folder")
}
// Resolve symlinks to prevent symlink-based escape
realPath, err := filepath.EvalSymlinks(resolved)
if err == nil {
if !strings.HasPrefix(realPath, projectRootWithSep) && realPath != projectRoot {
return "", fmt.Errorf("file access denied: symlink target resolves outside project folder")
}
return realPath, nil
}
return resolved, nil
}
// makeLoadBinary creates a Starlark builtin function load_binary(path, offset?, length?)
// that reads a binary file relative to the project root and returns a list of ints (0-255).
func makeLoadBinary(projectRoot string) *starlark.Builtin {
return starlark.NewBuiltin("load_binary", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var path string
var offset, length int
if err := starlark.UnpackPositionalArgs("load_binary", args, kwargs, 1, &path, &offset, &length); err != nil {
return nil, err
}
resolvedPath, err := validateScriptFilePath(projectRoot, path)
if err != nil {
return nil, fmt.Errorf("load_binary: %w", err)
}
data, err := os.ReadFile(resolvedPath)
if err != nil {
return nil, fmt.Errorf("load_binary: cannot read %s: %w", path, err)
}
if offset < 0 {
return nil, fmt.Errorf("load_binary: offset must be non-negative, got %d", offset)
}
if offset > len(data) {
return nil, fmt.Errorf("load_binary: offset %d exceeds file size %d", offset, len(data))
}
data = data[offset:]
if length > 0 {
if length > len(data) {
return nil, fmt.Errorf("load_binary: length %d exceeds available data %d", length, len(data))
}
data = data[:length]
}
result := make([]starlark.Value, len(data))
for i, b := range data {
result[i] = starlark.MakeInt(int(b))
}
return starlark.NewList(result), nil
})
}
// makeLoadText creates a Starlark builtin function load_text(path)
// that reads a text file relative to the project root and returns a list of strings (lines).
func makeLoadText(projectRoot string) *starlark.Builtin {
return starlark.NewBuiltin("load_text", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var path string
if err := starlark.UnpackPositionalArgs("load_text", args, kwargs, 1, &path); err != nil {
return nil, err
}
resolvedPath, err := validateScriptFilePath(projectRoot, path)
if err != nil {
return nil, fmt.Errorf("load_text: %w", err)
}
data, err := os.ReadFile(resolvedPath)
if err != nil {
return nil, fmt.Errorf("load_text: cannot read %s: %w", path, err)
}
text := strings.ReplaceAll(string(data), "\r\n", "\n")
text = strings.TrimRight(text, "\n")
var lines []string
if text == "" {
lines = []string{}
} else {
lines = strings.Split(text, "\n")
}
result := make([]starlark.Value, len(lines))
for i, line := range lines {
result[i] = starlark.String(line)
}
return starlark.NewList(result), nil
})
}
// readScriptMaxSteps reads the _P_SCRIPT_MAX_STEPS pragma from the given pragma set.
// Returns the configured value (must be > 0), or 1000000 as default.
func readScriptMaxSteps(ctx *CompilerContext, pragmaSetIndex int) uint64 {
const defaultSteps uint64 = 1000000
ps := ctx.Pragma.GetPragmaSetByIndex(pragmaSetIndex)
v := ps.GetPragma("_P_SCRIPT_MAX_STEPS")
if v == "" {
return defaultSteps
}
n, err := strconv.ParseUint(v, 10, 64)
if err != nil || n == 0 {
return defaultSteps
}
return n
} }
// ExecuteMacro executes a named macro with the given arguments and returns output lines // ExecuteMacro executes a named macro with the given arguments and returns output lines
// pragmaSetIndex is the index of the pragma set at the macro invocation call site. func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]string, error) {
func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaSetIndex int) ([]string, error) {
// Look up the macro // Look up the macro
macro, ok := ctx.ScriptMacros[macroName] macro, ok := ctx.ScriptMacros[macroName]
if !ok { if !ok {
@ -499,12 +130,6 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaS
} }
finalScript += "_macro()\n" finalScript += "_macro()\n"
// Use the source file where the macro was defined
starlarkFilename := macro.SourceFile
if starlarkFilename == "" {
starlarkFilename = "macro.star"
}
// Capture print output // Capture print output
var output bytes.Buffer var output bytes.Buffer
thread := &starlark.Thread{ thread := &starlark.Thread{
@ -514,14 +139,12 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaS
}, },
} }
// Set execution limit from pragma at call site or default // Set execution limit
thread.SetMaxExecutionSteps(readScriptMaxSteps(ctx, pragmaSetIndex)) thread.SetMaxExecutionSteps(1000000)
// Build predeclared: math + library globals + file I/O builtins + parameter bindings // Build predeclared: math + library globals + parameter bindings
predeclared := starlark.StringDict{ predeclared := starlark.StringDict{
"math": math.Module, "math": math.Module,
"load_binary": makeLoadBinary(ctx.ProjectRoot),
"load_text": makeLoadText(ctx.ProjectRoot),
} }
for k, v := range ctx.ScriptLibraryGlobals { for k, v := range ctx.ScriptLibraryGlobals {
predeclared[k] = v predeclared[k] = v
@ -531,19 +154,9 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaS
} }
// Execute // Execute
_, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared) _, err := starlark.ExecFile(thread, "macro.star", finalScript, predeclared)
if err != nil { if err != nil {
// Map error position back to macro definition site return nil, err
starLine := starlarkPosition(err)
if starLine > 0 && macro.SourceFile != "" {
idx := mapStarlarkLine(starLine, len(macro.Body), false) // macro is always wrapped
if idx >= 0 {
sourceLine := macro.StartLine + idx
msg := starlarkErrorMsg(err)
return nil, fmt.Errorf("Starlark error: at %s:%d: %s", macro.SourceFile, sourceLine, msg)
}
}
return nil, fmt.Errorf("Starlark error: %w", err)
} }
// Split output into lines // Split output into lines
@ -554,10 +167,7 @@ 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, err = expandVariables(outputStr, ctx) outputStr = 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
} }

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,6 @@ const (
FlagAbsolute FlagAbsolute
FlagZeroPage FlagZeroPage
FlagLabelRef FlagLabelRef
FlagRegister
) )
// Symbol represents a variable, constant, or label reference // Symbol represents a variable, constant, or label reference
@ -75,7 +74,6 @@ 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)
@ -145,20 +143,6 @@ 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
@ -356,13 +340,15 @@ 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)
// dissolvedVars is a set of REGISTER variables dissolved by the optimizer (do not warn) func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string {
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
@ -373,11 +359,6 @@ func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool, dissolvedVars m
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)
@ -389,6 +370,7 @@ func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool, dissolvedVars m
// 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)
@ -520,7 +502,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, dissolvedVars map[string]bool) []string { func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool) []string {
var lines []string var lines []string
hasVars := false hasVars := false
@ -533,10 +515,6 @@ func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool, dissolved
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, nil) lines := GenerateVariables(st, 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, nil); lines != nil { if lines := GenerateVariables(st, 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, nil) lines := GenerateVariables(st, 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, nil) varLines := GenerateVariables(st, 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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings := st.CheckUnused(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, nil) warnings = st.CheckUnused(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, nil) warnings := st.CheckUnused(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,101 +1067,9 @@ func TestUsageTracking(t *testing.T) {
} }
// No warnings should be generated // No warnings should be generated
warnings := st.CheckUnused(nil, nil) warnings := st.CheckUnused(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

@ -9,35 +9,32 @@ import (
) )
type Config struct { type Config struct {
EnableLoad bool EnableLoad bool
EnableImm bool EnableImm bool
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 {
all := ps.GetPragma("_P_OPT_ALL") != "" && ps.GetPragma("_P_OPT_ALL") != "0" all := ps.GetPragma("_P_OPT_ALL") != "" && ps.GetPragma("_P_OPT_ALL") != "0"
return &Config{ return &Config{
EnableLoad: all || (ps.GetPragma("_P_OPT_LOAD") != "" && ps.GetPragma("_P_OPT_LOAD") != "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"), 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"), 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 || c.EnableRegisterVars return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf || c.EnableStoreLoad
} }
// 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,17 +1,11 @@
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.
// Returns optimized lines and the set of dissolved REGISTER variable names func Optimize(lines []string, cfg *Config) []string {
// (variables that had all their stores/loads eliminated).
func Optimize(lines []SourceLine, cfg *Config) ([]string, map[string]bool) {
dissolved := map[string]bool{}
if cfg == nil || !cfg.Any() { if cfg == nil || !cfg.Any() {
return SourceLineTexts(lines), dissolved return lines
} }
parsed := parseLines(lines) parsed := parseLines(lines)
@ -19,7 +13,6 @@ func Optimize(lines []SourceLine, cfg *Config) ([]string, map[string]bool) {
if cfg.EnableStoreLoad { if cfg.EnableStoreLoad {
parsed = passStoreReload(parsed, cfg) parsed = passStoreReload(parsed, cfg)
parsed = passStoreTransfer(parsed, cfg)
} }
if cfg.EnableLoad { if cfg.EnableLoad {
parsed = passLoadElimination(parsed, cfg) parsed = passLoadElimination(parsed, cfg)
@ -33,41 +26,11 @@ func Optimize(lines []SourceLine, cfg *Config) ([]string, map[string]bool) {
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,30 +1,15 @@
package optimizer package optimizer
import ( import (
"strings"
"testing" "testing"
) )
func lines(s ...string) []SourceLine { func lines(s ...string) []string { return s }
out := make([]SourceLine, len(s))
for i, t := range s {
out[i] = SourceLine{Text: t, Origin: OriginGenerated}
}
return out
}
func verbatimLines(s ...string) []SourceLine {
out := make([]SourceLine, len(s))
for i, t := range s {
out[i] = SourceLine{Text: t, Origin: OriginAsm}
}
return out
}
func TestPassLoadElimination(t *testing.T) { func TestPassLoadElimination(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []SourceLine input []string
expected int // expected number of lines after optimization expected int // expected number of lines after optimization
}{ }{
{ {
@ -74,7 +59,7 @@ func TestPassLoadElimination(t *testing.T) {
func TestPassImmElimination(t *testing.T) { func TestPassImmElimination(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []SourceLine input []string
expected int expected int
}{ }{
{ {
@ -116,7 +101,7 @@ func TestPassImmElimination(t *testing.T) {
func TestPassJmpNext(t *testing.T) { func TestPassJmpNext(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []SourceLine input []string
expected int expected int
}{ }{
{ {
@ -151,7 +136,7 @@ func TestPassJmpNext(t *testing.T) {
func TestPassSelfAssignment(t *testing.T) { func TestPassSelfAssignment(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []SourceLine input []string
expected int expected int
}{ }{
{ {
@ -210,15 +195,6 @@ func TestPassLoadIO(t *testing.T) {
} }
}) })
t.Run("decimal IO load not eliminated", func(t *testing.T) {
parsed := parseLines(lines("\tlda 53266", "\tlda 53266"))
result := passLoadElimination(parsed, cfg)
cleaned := stripOptMarkers(result)
if len(cleaned) != 2 {
t.Errorf("expected 2 lines (decimal IO skip), got %d", len(cleaned))
}
})
t.Run("variable name not caught by IO", func(t *testing.T) { t.Run("variable name not caught by IO", func(t *testing.T) {
parsed := parseLines(lines("\tlda RASTER_LINE", "\tlda RASTER_LINE")) parsed := parseLines(lines("\tlda RASTER_LINE", "\tlda RASTER_LINE"))
result := passLoadElimination(parsed, cfg) result := passLoadElimination(parsed, cfg)
@ -256,7 +232,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 {
@ -264,25 +240,6 @@ func TestOptimizeIntegration(t *testing.T) {
} }
} }
func TestOptimizeStoreTransferIntegration(t *testing.T) {
input := lines(
"\tlda #$05",
"\tsta x",
"\tldy x",
"\tlda (zp),y",
)
cfg := &Config{EnableStoreLoad: true}
output, _ := Optimize(input, cfg)
if len(output) != 4 {
t.Fatalf("expected 4 lines, got %d:\n%v", len(output), output)
}
if output[2] != "\ttay" {
t.Errorf("expected ldy x to become tay via Optimize, got %q", output[2])
}
}
func TestOptimizeWithDebug(t *testing.T) { func TestOptimizeWithDebug(t *testing.T) {
input := lines( input := lines(
"\tlda x", "\tlda x",
@ -292,7 +249,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 {
@ -331,7 +288,7 @@ func TestPassStoreReload(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []SourceLine input []string
expected int expected int
}{ }{
{ {
@ -489,12 +446,13 @@ func TestPassStoreReloadIO(t *testing.T) {
t.Run("decimal address in IO range", func(t *testing.T) { t.Run("decimal address in IO range", func(t *testing.T) {
cfg := &Config{} cfg := &Config{}
cfg.IOMap[0xD020] = true cfg.IOMap[0xD020] = true
// Decimal 53280 = $D020, now caught by IOMap regardless of base // Decimal 53280 = $D020, but IOMap uses hex lookup
// The pass checks $ prefix only, decimal addresses won't be caught by IOMap
parsed := parseLines(lines("\tsta 53280", "\tlda 53280")) parsed := parseLines(lines("\tsta 53280", "\tlda 53280"))
result := passStoreReload(parsed, cfg) result := passStoreReload(parsed, cfg)
cleaned := stripOptMarkers(result) cleaned := stripOptMarkers(result)
if len(cleaned) != 2 { if len(cleaned) != 1 {
t.Errorf("expected 2 lines (decimal I/O protected), got %d", len(cleaned)) t.Errorf("expected 1 line (decimal not caught by I/O), got %d", len(cleaned))
} }
}) })
@ -538,8 +496,8 @@ func TestPassStoreReloadIO(t *testing.T) {
{Start: 0xD000, End: 0xDFFF}, {Start: 0xD000, End: 0xDFFF},
{Start: 0xDC00, End: 0xDC0F}, {Start: 0xDC00, End: 0xDC0F},
}) })
tests := []struct { tests := []struct{
addr string addr string
expect int expect int
}{ }{
{"$D020", 2}, {"$D020", 2},
@ -549,7 +507,7 @@ func TestPassStoreReloadIO(t *testing.T) {
{"$E000", 1}, {"$E000", 1},
} }
for _, tt := range tests { for _, tt := range tests {
parsed := parseLines(lines("\tsta "+tt.addr, "\tlda "+tt.addr)) parsed := parseLines(lines("\tsta " + tt.addr, "\tlda " + tt.addr))
result := passStoreReload(parsed, cfg) result := passStoreReload(parsed, cfg)
cleaned := stripOptMarkers(result) cleaned := stripOptMarkers(result)
if len(cleaned) != tt.expect { if len(cleaned) != tt.expect {
@ -558,803 +516,3 @@ func TestPassStoreReloadIO(t *testing.T) {
} }
}) })
} }
func TestPassStoreTransfer(t *testing.T) {
cfg := &Config{}
t.Run("sta x then ldy x becomes tay", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[0] != "\tsta x" || out[1] != "\ttay" {
t.Errorf("got %v", out)
}
})
t.Run("sta x then ldx x becomes tax", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldx x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[0] != "\tsta x" || out[1] != "\ttax" {
t.Errorf("got %v", out)
}
})
t.Run("comment between is skipped", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "; source", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 3 || out[2] != "\ttay" {
t.Errorf("got %v", out)
}
})
t.Run("label between blocks transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "label", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 3 || out[2] != "\tldy x" {
t.Errorf("got %v", out)
}
})
t.Run("IO address not transferred", func(t *testing.T) {
ioCfg := &Config{}
ioCfg.IOMap[0xD020] = true
parsed := parseLines(lines("\tsta $D020", "\tldy $D020"))
result := passStoreTransfer(parsed, ioCfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy $D020" {
t.Errorf("got %v", out)
}
})
t.Run("operand mismatch no transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldy y"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy y" {
t.Errorf("got %v", out)
}
})
t.Run("inline comment on load blocks transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tldy x ; note"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy x ; note" {
t.Errorf("got %v", out)
}
})
t.Run("indexed store not transferred", func(t *testing.T) {
parsed := parseLines(lines("\tsta (zp),y", "\tldy (zp),y"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tldy (zp),y" {
t.Errorf("got %v", out)
}
})
t.Run("lda between blocks transfer", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tlda y", "\tldy x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 3 || out[2] != "\tldy x" {
t.Errorf("got %v", out)
}
})
t.Run("sta x then lda x not transferred", func(t *testing.T) {
parsed := parseLines(lines("\tsta x", "\tlda x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tlda x" {
t.Errorf("got %v", out)
}
})
t.Run("sta as last line no transfer", func(t *testing.T) {
parsed := parseLines(lines("\tlda #1", "\tsta x"))
result := passStoreTransfer(parsed, cfg)
out := linesToString(stripOptMarkers(result))
if len(out) != 2 || out[1] != "\tsta x" {
t.Errorf("got %v", out)
}
})
}
func TestPassRegDead_DeadStore(t *testing.T) {
input := lines(
"\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 := lines(
"\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) {
// sta regVar before a label — no lda regVar anywhere → globally dead
input := lines(
"\tlda #$05",
"\tsta myFunc_temp",
"myskip:",
"\tnop",
)
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 — globally dead")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "sta myFunc_temp") {
t.Errorf("expected sta myFunc_temp to be removed (globally dead), got:\n%s", joined)
}
}
func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) {
input := lines(
"\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 := lines(
"\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 := lines(
"\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 := lines(
"\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_GloballyDeadBeforeJsr(t *testing.T) {
// sta regVar; jsr foo; no lda regVar anywhere → globally dead
// jsr does not protect a store with zero readers.
input := lines(
"\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 be dissolved — globally dead")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "sta call_val") {
t.Errorf("expected sta call_val to be removed (globally dead), got:\n%s", joined)
}
}
func TestPassRegDead_JsrProtectsStoreWhenReloaded(t *testing.T) {
// sta regVar; jsr foo; lda regVar → store kept
// lda exists (pre-scan passes), but jsr blocks local scan before reaching it.
// The callee may modify A, so the reload after jsr is genuine.
input := lines(
"\tlda 53281",
"\tsta call_val",
"\tjsr helper",
"\tlda call_val",
"\tsta 53280",
"\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 — lda exists, jsr protects store")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta call_val") {
t.Errorf("expected sta call_val to be kept (jsr before reload), got:\n%s", joined)
}
}
func TestPassRegDead_InitValueFlowsThrough(t *testing.T) {
// BYTE REGISTER x = 42; POKE $d020, x → value flows through A, never touches RAM
input := lines(
"\tlda #$2a",
"\tsta test_x",
"\tlda test_x",
"\tsta 53280",
"\trts",
)
cfg := &Config{
EnableRegisterVars: true,
EnableStoreLoad: true,
RegisterVars: map[string]bool{
"test_x": true,
},
}
output, dissolved := Optimize(input, cfg)
if !dissolved["test_x"] {
t.Error("test_x should be dissolved — init value flows through A to POKE")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "test_x") {
t.Errorf("expected no reference to test_x in output, got:\n%s", joined)
}
if !strings.Contains(joined, "lda #$2a") {
t.Error("init value lda #$2a should survive")
}
}
func TestPassRegDead_GloballyDeadBeforeLabel(t *testing.T) {
// sta regVar before a label, no lda regVar anywhere → globally dead
// Typical copy-loop pattern: the value flows through A, never reloaded.
input := lines(
"_LOOPSTART",
"\tlda (src),y",
"\tsta loop_val",
"\tsta (dst),y",
"\tbne _SKIP",
"_SKIP:",
"\tjmp _LOOPSTART",
"\trts",
)
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"loop_val": true,
},
}
output, dissolved := Optimize(input, cfg)
if !dissolved["loop_val"] {
t.Error("loop_val should be dissolved — globally dead (no lda anywhere)")
}
joined := strings.Join(output, "\n")
if strings.Contains(joined, "sta loop_val") {
t.Errorf("expected sta loop_val to be removed (globally dead), got:\n%s", joined)
}
}
func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
// sta regVar before a label, but lda regVar exists elsewhere → not globally dead
// Falls through to local scan. With jsr before the lda, store is kept.
input := lines(
"\tsta spill_me_bg",
"\tlda 53282",
"_SKIP:",
"\tjsr helper",
"\tlda spill_me_bg",
"\tsta 53282",
"\trts",
)
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"spill_me_bg": true,
},
}
output, dissolved := Optimize(input, cfg)
if dissolved["spill_me_bg"] {
t.Error("spill_me_bg should NOT be dissolved — has a real lda")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta spill_me_bg") {
t.Errorf("expected sta spill_me_bg to be kept, got:\n%s", joined)
}
}
func TestPassRegDead_ReadModifyWrite(t *testing.T) {
tests := []struct {
name string
opcode string
lines []SourceLine
}{
{
name: "dec reads stored value",
opcode: "dec",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tdec rmw_var",
"\trts",
),
},
{
name: "inc reads stored value",
opcode: "inc",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tinc rmw_var",
"\trts",
),
},
{
name: "adc reads stored value",
opcode: "adc",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tclc",
"\tadc rmw_var",
"\trts",
),
},
{
name: "sbc reads stored value",
opcode: "sbc",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tsec",
"\tsbc rmw_var",
"\trts",
),
},
{
name: "and reads stored value",
opcode: "and",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tand rmw_var",
"\trts",
),
},
{
name: "ora reads stored value",
opcode: "ora",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tora rmw_var",
"\trts",
),
},
{
name: "eor reads stored value",
opcode: "eor",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\teor rmw_var",
"\trts",
),
},
{
name: "cmp reads stored value",
opcode: "cmp",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tcmp rmw_var",
"\trts",
),
},
{
name: "ldx reads stored value",
opcode: "ldx",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tldx rmw_var",
"\trts",
),
},
{
name: "ldy reads stored value",
opcode: "ldy",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tldy rmw_var",
"\trts",
),
},
{
name: "asl reads stored value",
opcode: "asl",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tasl rmw_var",
"\trts",
),
},
{
name: "lsr reads stored value",
opcode: "lsr",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tlsr rmw_var",
"\trts",
),
},
{
name: "rol reads stored value",
opcode: "rol",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\trol rmw_var",
"\trts",
),
},
{
name: "ror reads stored value",
opcode: "ror",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tror rmw_var",
"\trts",
),
},
{
name: "bit reads stored value",
opcode: "bit",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tbit rmw_var",
"\trts",
),
},
{
name: "cpx reads stored value",
opcode: "cpx",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tcpx rmw_var",
"\trts",
),
},
{
name: "cpy reads stored value",
opcode: "cpy",
lines: lines(
"\tlda #$05",
"\tsta rmw_var",
"\tcpy rmw_var",
"\trts",
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"rmw_var": true,
},
}
output, dissolved := Optimize(tt.lines, cfg)
if dissolved["rmw_var"] {
t.Errorf("rmw_var should NOT be dissolved — %s reads its value", tt.opcode)
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta rmw_var") {
t.Errorf("expected sta rmw_var to be kept (followed by %s), got:\n%s", tt.opcode, joined)
}
})
}
}
func TestPassRegDead_JmpDoesNotKillStore(t *testing.T) {
// sta regVar; ...(then body)...; jmp _END; _ELSE:; ...; _END:; ldy regVar
// The jmp in the THEN body should NOT kill the store because
// the jump target _END reaches code that reads regVar.
input := lines(
"\tldy #0",
"\tlda (zp),y",
"\tsta if_val",
"\tlda test_i",
"\tcmp #$06",
"\tbne _I1",
"\tlda (zp),y",
"\tsta other",
"\tjmp _I2",
"_I1",
"\tldy #1",
"\tlda (zp),y",
"\tsta other",
"_I2",
"\tldy if_val",
"\tlda (zp),y",
"\trts",
)
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"if_val": true,
},
}
output, dissolved := Optimize(input, cfg)
if dissolved["if_val"] {
t.Error("if_val should NOT be dissolved — ldy if_val reads its value")
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta if_val") {
t.Errorf("expected sta if_val to be kept (jmp in THEN should not kill it), got:\n%s", joined)
}
}
func TestParseLinesOrigin(t *testing.T) {
t.Run("generated tab line is code", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "\tlda x", Origin: OriginGenerated}})
if len(parsed) != 1 || !parsed[0].isCode {
t.Errorf("generated tab line should be code, got %+v", parsed)
}
})
t.Run("generated space line is code", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: " lda x", Origin: OriginGenerated}})
if len(parsed) != 1 || !parsed[0].isCode {
t.Errorf("generated space-indented line should be code, got %+v", parsed)
}
})
t.Run("verbatim ASM tab line is a barrier, not code", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "\tlda x", Origin: OriginAsm}})
if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel {
t.Errorf("verbatim ASM line should be a barrier, got %+v", parsed)
}
})
t.Run("verbatim SCRIPT tab line is a barrier", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "\tsta $d020", Origin: OriginScript}})
if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel {
t.Errorf("verbatim SCRIPT line should be a barrier, got %+v", parsed)
}
})
t.Run("verbatim ASM comment stays a comment", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "; note", Origin: OriginAsm}})
if len(parsed) != 1 || !parsed[0].isComment {
t.Errorf("verbatim ASM comment should be a comment, got %+v", parsed)
}
})
t.Run("verbatim ASM label stays a barrier", func(t *testing.T) {
parsed := parseLines([]SourceLine{{Text: "h_dispatch:", Origin: OriginAsm}})
if len(parsed) != 1 || parsed[0].isCode || !parsed[0].isLabel {
t.Errorf("verbatim ASM label should be a barrier, got %+v", parsed)
}
})
}
func TestOptimizeSkipsVerbatimTransfer(t *testing.T) {
// A tab-indented ASM block line must NOT be rewritten, even though it
// looks like generated code: provenance trumps indentation.
input := []SourceLine{
{Text: "\tsta x", Origin: OriginAsm},
{Text: "\tldy x", Origin: OriginAsm},
}
cfg := &Config{EnableStoreLoad: true}
output, _ := Optimize(input, cfg)
if len(output) != 2 || output[0] != "\tsta x" || output[1] != "\tldy x" {
t.Errorf("verbatim ASM lines must not be optimized, got %v", output)
}
}
func TestPassRegDead_OriginAffectsReadDetection(t *testing.T) {
regVars := map[string]bool{"bg": true}
t.Run("generated read keeps store", func(t *testing.T) {
input := []SourceLine{
{Text: "\tlda 53281", Origin: OriginGenerated},
{Text: "\tsta bg", Origin: OriginGenerated},
{Text: "\tlda bg", Origin: OriginGenerated},
{Text: "\tsta 53280", Origin: OriginGenerated},
{Text: "\trts", Origin: OriginGenerated},
}
out, dissolved := Optimize(input, &Config{EnableRegisterVars: true, RegisterVars: regVars})
if dissolved["bg"] {
t.Error("bg should NOT dissolve: a generated read exists")
}
if !strings.Contains(strings.Join(out, "\n"), "sta bg") {
t.Errorf("expected sta bg kept (generated read), got:\n%s", strings.Join(out, "\n"))
}
})
t.Run("verbatim ASM read does not keep store", func(t *testing.T) {
input := []SourceLine{
{Text: "\tlda 53281", Origin: OriginGenerated},
{Text: "\tsta bg", Origin: OriginGenerated},
{Text: "\tlda bg", Origin: OriginAsm},
{Text: "\tsta 53280", Origin: OriginGenerated},
{Text: "\trts", Origin: OriginGenerated},
}
out, _ := Optimize(input, &Config{EnableRegisterVars: true, RegisterVars: regVars})
joined := strings.Join(out, "\n")
if strings.Contains(joined, "sta bg") {
t.Errorf("expected sta bg removed (verbatim ASM read is not a generated read), got:\n%s", joined)
}
// The verbatim ASM line itself must be preserved untouched.
if !strings.Contains(joined, "\tlda bg") {
t.Errorf("expected verbatim lda bg preserved, got:\n%s", joined)
}
})
}

View file

@ -76,12 +76,14 @@ func isIndexedOperand(operand string) bool {
return strings.Contains(operand, "(") || strings.Contains(operand, ",") return strings.Contains(operand, "(") || strings.Contains(operand, ",")
} }
// isIOAddr returns true if operand is a numeric address (hex or decimal) marked // isIOAddr returns true if operand is a hex address marked as I/O in the config.
// as I/O in the config. Symbolic names parse to -1 and are never treated as I/O.
func isIOAddr(operand string, cfg *Config) bool { func isIOAddr(operand string, cfg *Config) bool {
if cfg == nil { if cfg == nil {
return false return false
} }
if !strings.HasPrefix(operand, "$") {
return false
}
addr := parseHexOrDec(operand) addr := parseHexOrDec(operand)
return addr >= 0 && addr < 65536 && cfg.IOMap[addr] return addr >= 0 && addr < 65536 && cfg.IOMap[addr]
} }

View file

@ -1,106 +0,0 @@
package optimizer
// passRegDead eliminates dead stores to REGISTER variables.
// Two-phase approach:
// 1. Global pre-scan: if no instruction reads regVar anywhere in the input,
// all stores to that variable are provably dead (labels are irrelevant).
// 2. Per-store local scan: for variables that DO have at least one read,
// scan forward through the current basic block.
//
// This is safe because REGISTER variables have a contract: nothing external
// can observe their memory location (ASM/Script/Macro references are compile errors).
func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
// Phase 1 — global pre-scan: which REGISTER vars have at least one read?
hasRead := make(map[string]bool, len(registerVars))
for _, l := range lines {
if l.isCode && l.operand != "" && registerVars[l.operand] && readsFrom(l.opcode, l.operand, l.operand) {
hasRead[l.operand] = true
}
}
// Phase 2 — per-store decision
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 !hasRead[line.operand] {
// Globally dead — no read anywhere in the program
continue
}
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 instruction that reads the operand 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/brk/rti — execution ends here
}
if l.isCode && readsFrom(l.opcode, l.operand, operand) {
return false // the value IS read from memory later
}
}
return true // end of block reached with no matching load
}
// readsFrom returns true if the instruction reads from the given memory operand.
// Covers loads (lda/ldx/ldy), compares (cmp/cpx/cpy), ALU ops (adc/sbc/and/ora/eor/bit),
// and read-modify-write instructions (dec/inc/asl/lsr/rol/ror).
func readsFrom(opcode, operand, varName string) bool {
if operand != varName {
return false
}
switch opcode {
case "lda", "ldx", "ldy":
return true
case "adc", "sbc", "and", "ora", "eor", "cmp", "cpx", "cpy", "bit":
return true
case "dec", "inc", "asl", "lsr", "rol", "ror":
return true
}
return false
}
// 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", "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"
}

View file

@ -79,9 +79,8 @@ func isSafeStldOperand(operand string, cfg *Config) bool {
return false return false
} }
// Check if operand is a direct numeric address (hex or decimal) in an I/O region. // Check if operand is a direct hex address in an I/O region
// Symbolic names parse to -1 and are never treated as I/O. if strings.HasPrefix(operand, "$") {
if cfg != nil {
addr := parseHexOrDec(operand) addr := parseHexOrDec(operand)
if addr >= 0 && addr < 65536 && cfg.IOMap[addr] { if addr >= 0 && addr < 65536 && cfg.IOMap[addr] {
return false return false

View file

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

View file

@ -41,36 +41,9 @@ type asmLine struct {
operand string operand string
} }
// Origin identifies the source of an output line. The optimizer must know func parseLines(lines []string) []asmLine {
// whether a line was produced by the compiler itself (and is therefore safe
// to optimize) or emitted verbatim from an ASM block or SCRIPT output.
type Origin int
const (
OriginGenerated Origin = iota // compiler-generated code (optimizable)
OriginAsm // handwritten ASM block content (verbatim)
OriginScript // SCRIPT print() output (verbatim)
)
// SourceLine is a single output line together with its provenance.
type SourceLine struct {
Text string
Origin Origin
}
// SourceLineTexts extracts just the text from a slice of source lines.
func SourceLineTexts(lines []SourceLine) []string {
out := make([]string, len(lines))
for i, l := range lines {
out[i] = l.Text
}
return out
}
func parseLines(lines []SourceLine) []asmLine {
var result []asmLine var result []asmLine
for _, sl := range lines { for _, l := range lines {
l := sl.Text
al := asmLine{text: l} al := asmLine{text: l}
if l == "" { if l == "" {
@ -91,21 +64,14 @@ func parseLines(lines []SourceLine) []asmLine {
continue continue
} }
// Only compiler-generated, indented lines are treated as optimizable if l[0] == '\t' {
// instructions. ACME requires labels at column 0, so any leading al.isCode = true
// whitespace means "not a label" — the exact character (tab or space)
// is irrelevant. Generated labels and any verbatim ASM/SCRIPT line
// become barriers.
if sl.Origin == OriginGenerated && isIndented(l) {
parts := strings.Fields(l) parts := strings.Fields(l)
if len(parts) > 0 { if len(parts) > 0 {
al.isCode = true
al.opcode = strings.ToLower(parts[0]) al.opcode = strings.ToLower(parts[0])
if len(parts) > 1 { }
al.operand = parts[1] if len(parts) > 1 {
} al.operand = parts[1]
} else {
al.isLabel = true
} }
} else { } else {
al.isLabel = true al.isLabel = true
@ -116,12 +82,6 @@ func parseLines(lines []SourceLine) []asmLine {
return result return result
} }
// isIndented reports whether a line has leading whitespace (an instruction),
// as opposed to a label which starts at column 0.
func isIndented(l string) bool {
return len(l) > 0 && (l[0] == ' ' || l[0] == '\t')
}
// stripOptMarkers removes @@OPT comment lines from the output // stripOptMarkers removes @@OPT comment lines from the output
func stripOptMarkers(lines []asmLine) []asmLine { func stripOptMarkers(lines []asmLine) []asmLine {
var result []asmLine var result []asmLine
@ -146,3 +106,4 @@ func linesToString(lines []asmLine) []string {
func skipJmpMarker(line asmLine) bool { func skipJmpMarker(line asmLine) bool {
return line.optMarker return line.optMarker
} }

View file

@ -170,21 +170,7 @@ func (p *preproc) run(root string) ([]Line, error) {
p.inScript = false p.inScript = false
p.inScriptLibrary = false p.inScriptLibrary = false
p.inScriptMacro = false p.inScriptMacro = false
// Emit an empty Source line as a block boundary marker so the continue // don't emit ENDSCRIPT marker
// compiler can distinguish consecutive SCRIPT blocks via kind
// transitions. The compiler skips empty Source lines, so this
// serves purely as a transition trigger.
if includeSource {
out = append(out, Line{
RawText: raw,
Text: "",
Filename: currFrame.path,
LineNo: currFrame.line,
Kind: Source,
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
})
}
continue
} }
// Determine the kind based on which mode we're in // Determine the kind based on which mode we're in
kind := Script kind := Script

View file

@ -151,10 +151,9 @@ func TestPreProcess_ScriptBlock(t *testing.T) {
t.Fatalf("PreProcess failed: %v", err) t.Fatalf("PreProcess failed: %v", err)
} }
// SCRIPT and ENDSCRIPT markers are stripped, but ENDSCRIPT emits // SCRIPT and ENDSCRIPT markers are stripped
// an empty Source boundary marker if len(lines) != 3 {
if len(lines) != 4 { t.Fatalf("expected 3 lines, got %d", len(lines))
t.Fatalf("expected 4 lines, got %d", len(lines))
} }
// Script content should NOT be processed // Script content should NOT be processed
@ -172,20 +171,12 @@ func TestPreProcess_ScriptBlock(t *testing.T) {
t.Errorf("expected Kind=Script, got %v", lines[1].Kind) t.Errorf("expected Kind=Script, got %v", lines[1].Kind)
} }
// Line 2 is the ENDSCRIPT boundary marker (empty Source line)
if lines[2].Kind != Source {
t.Errorf("line 2: expected Kind=Source (ENDSCRIPT boundary), got %v", lines[2].Kind)
}
if lines[2].Text != "" {
t.Errorf("line 2: expected empty text, got %q", lines[2].Text)
}
// After ENDSCRIPT, defines work again // After ENDSCRIPT, defines work again
if lines[3].Text != "LDA #100" { if lines[2].Text != "LDA #100" {
t.Errorf("expected 'LDA #100', got %q", lines[3].Text) t.Errorf("expected 'LDA #100', got %q", lines[2].Text)
} }
if lines[3].Kind != Source { if lines[2].Kind != Source {
t.Errorf("expected Kind=Source, got %v", lines[3].Kind) t.Errorf("expected Kind=Source, got %v", lines[2].Kind)
} }
} }
@ -271,9 +262,8 @@ func TestPreProcess_CommentInScriptBlock(t *testing.T) {
t.Fatalf("PreProcess failed: %v", err) t.Fatalf("PreProcess failed: %v", err)
} }
// 2 script lines + 1 ENDSCRIPT boundary marker + 0 trailing source = 3 lines if len(lines) != 2 {
if len(lines) != 3 { t.Fatalf("expected 2 lines, got %d", len(lines))
t.Fatalf("expected 3 lines, got %d", len(lines))
} }
// Comments should be preserved in Script blocks // Comments should be preserved in Script blocks
@ -283,11 +273,6 @@ func TestPreProcess_CommentInScriptBlock(t *testing.T) {
if lines[1].Text != " y = 2 // another one" { if lines[1].Text != " y = 2 // another one" {
t.Errorf("expected comment preserved, got %q", lines[1].Text) t.Errorf("expected comment preserved, got %q", lines[1].Text)
} }
// Line 2 is ENDSCRIPT boundary marker
if lines[2].Kind != Source || lines[2].Text != "" {
t.Errorf("line 2: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[2].Kind, lines[2].Text)
}
} }
func TestPreProcess_RawTextPreservation(t *testing.T) { func TestPreProcess_RawTextPreservation(t *testing.T) {
@ -898,7 +883,6 @@ func TestPreProcess_MixedBlocksAndComments(t *testing.T) {
{"LDA #10", Source}, {"LDA #10", Source},
{" lda #X // asm comment", Assembler}, {" lda #X // asm comment", Assembler},
{" y = X // script comment", Script}, {" y = X // script comment", Script},
{"", Source}, // ENDSCRIPT boundary marker
{"STA $D020", Source}, {"STA $D020", Source},
} }
@ -954,17 +938,12 @@ func TestPreProcess_EmptyScriptBlock(t *testing.T) {
t.Fatalf("PreProcess failed: %v", err) t.Fatalf("PreProcess failed: %v", err)
} }
// Empty script emits an ENDSCRIPT boundary marker + NOP if len(lines) != 1 {
if len(lines) != 2 { t.Fatalf("expected 1 line, got %d", len(lines))
t.Fatalf("expected 2 lines, got %d", len(lines))
} }
if lines[0].Kind != Source || lines[0].Text != "" { if lines[0].Text != "NOP" {
t.Errorf("line 0: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[0].Kind, lines[0].Text) t.Errorf("expected 'NOP', got %q", lines[0].Text)
}
if lines[1].Text != "NOP" {
t.Errorf("expected 'NOP', got %q", lines[1].Text)
} }
} }
@ -984,9 +963,9 @@ func TestPreProcess_ScriptLibraryBlock(t *testing.T) {
t.Fatalf("PreProcess failed: %v", err) t.Fatalf("PreProcess failed: %v", err)
} }
// Should have 2 script lines + 1 ENDSCRIPT boundary + 1 source line = 4 lines // Should have 2 script lines + 1 source line
if len(lines) != 4 { if len(lines) != 3 {
t.Fatalf("expected 4 lines, got %d", len(lines)) t.Fatalf("expected 3 lines, got %d", len(lines))
} }
// Script library lines should have ScriptLibrary kind // Script library lines should have ScriptLibrary kind
@ -1001,20 +980,12 @@ func TestPreProcess_ScriptLibraryBlock(t *testing.T) {
t.Errorf("expected Kind=ScriptLibrary, got %v", lines[1].Kind) t.Errorf("expected Kind=ScriptLibrary, got %v", lines[1].Kind)
} }
// Line 2 is the ENDSCRIPT boundary marker
if lines[2].Kind != Source {
t.Errorf("line 2: expected Kind=Source (ENDSCRIPT boundary), got %v", lines[2].Kind)
}
if lines[2].Text != "" {
t.Errorf("line 2: expected empty text, got %q", lines[2].Text)
}
// Source line after ENDSCRIPT // Source line after ENDSCRIPT
if lines[3].Kind != Source { if lines[2].Kind != Source {
t.Errorf("expected Kind=Source, got %v", lines[3].Kind) t.Errorf("expected Kind=Source, got %v", lines[2].Kind)
} }
if lines[3].Text != "NOP" { if lines[2].Text != "NOP" {
t.Errorf("expected 'NOP', got %q", lines[3].Text) t.Errorf("expected 'NOP', got %q", lines[2].Text)
} }
} }
@ -1035,29 +1006,18 @@ func TestPreProcess_ScriptVsScriptLibrary(t *testing.T) {
t.Fatalf("PreProcess failed: %v", err) t.Fatalf("PreProcess failed: %v", err)
} }
// Two blocks → 1 lib line + 1 lib ENDSCRIPT boundary + 1 script line + 1 script ENDSCRIPT boundary = 4 lines if len(lines) != 2 {
if len(lines) != 4 { t.Fatalf("expected 2 lines, got %d", len(lines))
t.Fatalf("expected 4 lines, got %d", len(lines))
} }
// Line 0: SCRIPT LIBRARY content // First line is from SCRIPT LIBRARY
if lines[0].Kind != ScriptLibrary { if lines[0].Kind != ScriptLibrary {
t.Errorf("line 0: expected ScriptLibrary, got %v", lines[0].Kind) t.Errorf("line 0: expected ScriptLibrary, got %v", lines[0].Kind)
} }
// Line 1: ENDSCRIPT boundary for library // Second line is from regular SCRIPT
if lines[1].Kind != Source || lines[1].Text != "" { if lines[1].Kind != Script {
t.Errorf("line 1: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[1].Kind, lines[1].Text) t.Errorf("line 1: expected Script, got %v", lines[1].Kind)
}
// Line 2: regular SCRIPT content
if lines[2].Kind != Script {
t.Errorf("line 2: expected Script, got %v", lines[2].Kind)
}
// Line 3: ENDSCRIPT boundary for script
if lines[3].Kind != Source || lines[3].Text != "" {
t.Errorf("line 3: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[3].Kind, lines[3].Text)
} }
} }
@ -1076,9 +1036,9 @@ func TestPreProcess_ScriptMacroBlock(t *testing.T) {
t.Fatalf("PreProcess failed: %v", err) t.Fatalf("PreProcess failed: %v", err)
} }
// Should have header + body line + ENDSCRIPT boundary + source line = 4 lines // Should have header + body line + source line = 3 lines
if len(lines) != 4 { if len(lines) != 3 {
t.Fatalf("expected 4 lines, got %d", len(lines)) t.Fatalf("expected 3 lines, got %d", len(lines))
} }
// First line is the header (also ScriptMacroDef kind) // First line is the header (also ScriptMacroDef kind)
@ -1094,20 +1054,12 @@ func TestPreProcess_ScriptMacroBlock(t *testing.T) {
t.Errorf("line 1: expected ScriptMacroDef, got %v", lines[1].Kind) t.Errorf("line 1: expected ScriptMacroDef, got %v", lines[1].Kind)
} }
// Third line is ENDSCRIPT boundary marker // Third line is source
if lines[2].Kind != Source { if lines[2].Kind != Source {
t.Errorf("line 2: expected Source (ENDSCRIPT boundary), got %v", lines[2].Kind) t.Errorf("line 2: expected Source, got %v", lines[2].Kind)
} }
if lines[2].Text != "" { if lines[2].Text != "NOP" {
t.Errorf("line 2: expected empty text, got %q", lines[2].Text) t.Errorf("line 2: expected 'NOP', got %q", lines[2].Text)
}
// Fourth line is source
if lines[3].Kind != Source {
t.Errorf("line 3: expected Source, got %v", lines[3].Kind)
}
if lines[3].Text != "NOP" {
t.Errorf("line 3: expected 'NOP', got %q", lines[3].Text)
} }
} }

View file

@ -102,34 +102,6 @@ func ToUpper(s string) string {
return strings.ToUpper(s) return strings.ToUpper(s)
} }
// NormalizeCommas inserts spaces around commas outside quoted strings,
// then collapses multiple spaces. This ensures commas are treated as
// separate tokens when passed to ParseParams.
func NormalizeCommas(s string) string {
var result strings.Builder
inString := false
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '"' {
inString = !inString
result.WriteByte(ch)
continue
}
if !inString && ch == ',' {
result.WriteByte(' ')
result.WriteByte(',')
result.WriteByte(' ')
} else {
result.WriteByte(ch)
}
}
return NormalizeSpaces(result.String())
}
// ValidateIdentifier checks if s is a valid identifier (starts with letter/underscore, continues with alphanumeric/underscore) // ValidateIdentifier checks if s is a valid identifier (starts with letter/underscore, continues with alphanumeric/underscore)
func ValidateIdentifier(s string) bool { func ValidateIdentifier(s string) bool {
if len(s) == 0 { if len(s) == 0 {

View file

@ -88,40 +88,12 @@ initialize()
BYTE variables store 8-bit values (0-255). BYTE variables store 8-bit values (0-255).
```c65 ```c65
BYTE count // Uninitialized (0) BYTE count // Uninitialized
BYTE speed = 5 // Initialized to 5 BYTE speed = 5 // Initialized to 5
BYTE REGISTER temp // Register-hinted (default 0)
BYTE REGISTER scratch = 0 // Register-hinted with init value
BYTE screen @ $D020 // Memory-mapped to specific address BYTE screen @ $D020 // Memory-mapped to specific address
BYTE CONST MAX_SPEED = 10 // Constant BYTE CONST MAX_SPEED = 10 // Constant (recommended over #DEFINE)
``` ```
### REGISTER Variables
The `REGISTER` hint tells the optimizer that a BYTE variable's value only matters as
it flows through the computation — never its stored location. The compiler can keep
the value in a CPU register (A, X, or Y) and eliminate dead stores and memory
allocations entirely.
```c65
FUNC fast_copy
BYTE REGISTER b
b = PEEK $d011
b = b | 32
POKE $d011, b // b never touches RAM — flows through A
FEND
```
**Rules:**
- `BYTE REGISTER` is only valid inside `FUNC`/`FEND` blocks (function-local only)
- Cannot be combined with `@` (memory-mapped) or `CONST`
- Cannot be referenced from `ASM`, `SCRIPT`, or `MACRO` blocks via `|varname|`
- `WORD REGISTER` is not supported (the 6502 has no 16-bit ALU register)
- Without `--opt`, `REGISTER` behaves identically to a normal `BYTE`
- With `--opt`, the optimizer may dissolve the variable, eliminating its `!8`
allocation and all store/load operations
### WORD Variables ### WORD Variables
WORD variables store 16-bit values (0-65535). WORD variables store 16-bit values (0-65535).
@ -137,7 +109,7 @@ WORD CONST SCREEN_RAM = $0400 // Constant
### Memory-Mapped Variables ### Memory-Mapped Variables
Variables can be placed at specific addresses using `@`. Not compatible with `REGISTER`. Variables can be placed at specific addresses using `@`:
```c65 ```c65
BYTE borderColor @ $D020 // VIC-II border color BYTE borderColor @ $D020 // VIC-II border color
@ -206,13 +178,7 @@ result = 2+3*4 // Evaluates as (2+3)*4 = 20, NOT 2+(3*4) = 14
value = 100-20+5 // Evaluates as (100-20)+5 = 85 value = 100-20+5 // Evaluates as (100-20)+5 = 85
``` ```
These multi-term forms are folded at compile time and only work when every For complex expressions, use temporary variables:
term is a literal or `CONST` (write them without spaces). `*` and `/` are
**only** available in these constant expressions, not on runtime variables.
Runtime expressions involving a variable perform exactly one operation and
must be space-separated (`dest = a + b`). For anything more complex, use
temporary variables:
```c65 ```c65
// Instead of: result = (b - c) + a // Instead of: result = (b - c) + a
@ -272,10 +238,9 @@ WEND
### FOR Loops ### FOR Loops
Loop with automatic counter. The loop variable must be declared beforehand: Loop with automatic counter:
```c65 ```c65
BYTE i
FOR i = 0 TO 10 FOR i = 0 TO 10
screen = i screen = i
NEXT NEXT
@ -290,7 +255,6 @@ NEXT
Exit a loop early: Exit a loop early:
```c65 ```c65
BYTE i
FOR i = 0 TO 100 FOR i = 0 TO 100
IF i == 50 IF i == 50
BREAK BREAK
@ -390,17 +354,15 @@ Read a byte from memory:
```c65 ```c65
value = PEEK $D020 // Read from absolute address value = PEEK $D020 // Read from absolute address
byte = PEEK pointer // Read through a WORD pointer char = PEEK screenPtr[index] // Read with offset
byte = PEEK pointer // Read from pointer
``` ```
**Indexed access:** Add `[offset]` to read at pointer+offset. The offset can **Important:** For indexed access, the address must be a WORD variable in zero page.
be a constant or a BYTE variable, and the pointer **must** be a WORD variable
in zero page:
```c65 ```c65
WORD buffer @ $FB // Zero-page pointer WORD buffer @ $FB // Zero-page pointer
value = PEEK buffer[10] // Read buffer+10 value = PEEK buffer[10] // Read buffer+10
char = PEEK buffer[index] // Read buffer+index
``` ```
### POKE - Writing Memory ### POKE - Writing Memory
@ -408,19 +370,9 @@ char = PEEK buffer[index] // Read buffer+index
Write a byte to memory: Write a byte to memory:
```c65 ```c65
POKE $D020, 0 // Write to absolute address POKE $D020 WITH 0 // Write to absolute address
POKE pointer, value // Write through a WORD pointer POKE screenPtr[index] WITH char // Write with offset
``` POKE pointer WITH value // Write to pointer
**Indexed access:** As with PEEK, `[offset]` requires a zero-page WORD
pointer. This is handy for reaching into structured data through a base
pointer:
```c65
WORD vic @ $FB
POINTER vic TO $D000 // VIC-II base register
POKE vic[$20], 2 // Write border color ($D020) = red
POKE vic[$21], 0 // Write background color ($D021) = black
``` ```
### PEEKW - Reading 16-bit Words ### PEEKW - Reading 16-bit Words
@ -440,9 +392,8 @@ value = PEEKW buffer[10] // Read word at buffer+10
Write a 16-bit value to memory: Write a 16-bit value to memory:
```c65 ```c65
POKEW $0314, irqHandler // Set IRQ vector POKEW $0314 WITH irqHandler // Set IRQ vector
POKEW dataPtr, address // Write word through a pointer POKEW dataPtr[0] WITH address // Write word with offset
POKEW dataPtr[2], address // Write word at dataPtr+2 (zero-page pointer)
``` ```
### POINTER - Setting Pointers ### POINTER - Setting Pointers
@ -556,58 +507,6 @@ SCRIPT
ENDSCRIPT ENDSCRIPT
``` ```
#### File I/O
Scripts can read binary and text files at compile time using `load_binary()` and `load_text()`. These functions only allow access to files within the project folder (where the main .c65 file resides) and its subdirectories. Absolute paths and path traversal (`..`) are rejected.
**`load_binary(path, offset=0, length=0)`**
Reads a binary file and returns a list of integers (0-255). The optional `offset` parameter skips bytes at the start, and `length` limits how many bytes to read (0 = read to end of file).
```c65
SCRIPT
sprite = load_binary("assets/hero.spr")
print("hero_sprite:")
for i in range(0, len(sprite), 8):
row = ", ".join(["$%02x" % b for b in sprite[i:i+8]])
print(" !byte " + row)
ENDSCRIPT
```
**`load_text(path)`**
Reads a text file and returns a list of strings, one per line. Handles both `\n` (Unix) and `\r\n` (Windows) line endings.
```c65
SCRIPT
level = load_text("levels/lvl1.txt")
print("level_map:")
for y in range(len(level)):
print(" !text " + repr(level[y]))
ENDSCRIPT
```
**Example — resource loading with SCRIPT LIBRARY:**
```c65
SCRIPT LIBRARY
# Load resources into library globals
font_data = load_binary("assets/font.chr")
level = load_text("levels/lvl1.txt")
def emit_font():
print("font:")
for i in range(0, len(font_data), 8):
row = ", ".join(["$%02x" % b for b in font_data[i:i+8]])
print(" !byte " + row)
def emit_level():
print("level:")
for line in level:
print(" !text " + repr(line))
ENDSCRIPT
```
### SCRIPT LIBRARY Blocks ### SCRIPT LIBRARY Blocks
Define reusable Starlark functions that persist across all subsequent SCRIPT blocks: Define reusable Starlark functions that persist across all subsequent SCRIPT blocks:
@ -879,7 +778,7 @@ FEND
FUNC updateScreen FUNC updateScreen
BYTE color BYTE color
color = frameCount & $0F color = frameCount & $0F
POKE screenPtr, color POKE screenPtr[0] WITH color
frameCount++ frameCount++
FEND FEND
@ -906,7 +805,7 @@ FUNC clearScreen
WORD remaining = 1000 WORD remaining = 1000
WHILE remaining > 0 WHILE remaining > 0
POKE screenPtr, 32 // Space character POKE screenPtr[0] WITH 32 // Space character
screenPtr++ screenPtr++
remaining-- remaining--
WEND WEND
@ -919,7 +818,7 @@ FEND
// Print null-terminated string // Print null-terminated string
FUNC printString({WORD textPtr}) FUNC printString({WORD textPtr})
BYTE char BYTE char
char = PEEK textPtr char = PEEK textPtr[0]
WHILE char != 0 WHILE char != 0
ASM ASM
@ -927,7 +826,7 @@ FUNC printString({WORD textPtr})
jsr $FFD2 // CHROUT jsr $FFD2 // CHROUT
ENDASM ENDASM
textPtr++ textPtr++
char = PEEK textPtr char = PEEK textPtr[0]
WEND WEND
FEND FEND
``` ```
@ -942,11 +841,10 @@ BYTE spriteEnable @ VIC2+21
FUNC enableSprite({BYTE spriteNum}) FUNC enableSprite({BYTE spriteNum})
BYTE mask BYTE mask
BYTE i
mask = 1 mask = 1
FOR i = 0 TO spriteNum FOR i = 0 TO spriteNum
mask = mask << 1 mask = mask * 2
NEXT NEXT
spriteEnable = spriteEnable | mask spriteEnable = spriteEnable | mask
@ -958,14 +856,14 @@ FEND
For frequently accessed pointers, use zero page: For frequently accessed pointers, use zero page:
```c65 ```c65
WORD fastPtr @ $FB // Zero page = fast pointer access WORD fastPtr @ $FB // Zero page = fast indexed access
FUNC processBuffer({WORD buffer} {BYTE size}) FUNC processBuffer({WORD buffer} {BYTE size})
POINTER fastPtr TO buffer POINTER fastPtr TO buffer
WHILE size > 0 WHILE size > 0
BYTE value BYTE value
value = PEEK fastPtr value = PEEK fastPtr[0]
// Process value // Process value
fastPtr++ fastPtr++
size-- size--
@ -975,49 +873,46 @@ FEND
### Interrupt Handlers ### Interrupt Handlers
The C64 kernal calls the IRQ vector at `$0314` on every interrupt. Because
the kernal saves the CPU registers *before* calling the vector, your handler
does **not** need to push/pull A/X/Y itself. When finished, chain into the
kernal so it restores the registers and returns from the interrupt:
- `jmp $ea31` — let the kernal do its full IRQ work (scan keyboard, blink
cursor, update the jiffy clock, ...) and then `RTI`
- `jmp $ea81` — skip that work; just restore the saved registers and `RTI`
Once the vector is installed it keeps firing in the background, so the main
program can simply return to BASIC with `SUBEND` — the handler stays live.
```c65 ```c65
WORD CONST IRQ_VECTOR = $0314 WORD CONST IRQ_VECTOR = $0314
WORD handler = @myIRQ // Address of our IRQ handler WORD oldIRQ
FUNC installIRQ FUNC installIRQ
ASM ASM
sei // Disable interrupts while we patch sei // Disable interrupts
ENDASM ENDASM
POKEW IRQ_VECTOR, handler // Point the vector at our handler oldIRQ = PEEKW IRQ_VECTOR
POKEW IRQ_VECTOR WITH myIRQ
ASM ASM
cli // Re-enable interrupts cli // Enable interrupts
ENDASM ENDASM
FEND FEND
LABEL start
installIRQ()
SUBEND // Return to BASIC; the IRQ stays installed
// No register saving needed — the kernal already did it.
LABEL myIRQ LABEL myIRQ
// IRQ handler code
ASM ASM
inc $0400 // Do the IRQ work // Save registers
jmp $ea31 // Let the kernal finish the IRQ pha
txa
pha
tya
pha
// Do IRQ work
inc $d020
// Restore and return
pla
tay
pla
tax
pla
rti
ENDASM ENDASM
``` ```
See `examples/irq_demo/` for a complete, buildable version.
### Lookup Tables ### Lookup Tables
Generate tables at compile time: Generate tables at compile time:
@ -1062,6 +957,11 @@ flags = flags & %11111110 // Clear bit 0
// Toggle bit // Toggle bit
flags = flags ^ %00000001 // Toggle bit 0 flags = flags ^ %00000001 // Toggle bit 0
// Test bit
IF flags & %00000001
// Bit 0 is set
ENDIF
``` ```
--- ---
@ -1072,7 +972,7 @@ flags = flags ^ %00000001 // Toggle bit 0
```c65 ```c65
// Bad // Bad
POKE $D020, 5 POKE $D020 WITH 5
// Good // Good
BYTE CONST COLOR_GREEN = 5 BYTE CONST COLOR_GREEN = 5
@ -1094,14 +994,12 @@ WORD tempPtr @ $FD
Remember: left-to-right evaluation, no precedence! Remember: left-to-right evaluation, no precedence!
```c65 ```c65
// Constant expressions fold left-to-right at compile time. // Be careful with expressions
// Write them without spaces: result = 2 + 3 * 4 // = 20, not 14
result = 2+3*4 // = (2+3)*4 = 20, not 14
// Runtime expressions (with variables) do ONE operation per // Use temps for clarity
// statement and must be space-separated. Use temps to order them: temp = 3 * 4
temp = b + c result = 2 + temp // Now = 14
result = a + temp
``` ```
### 4. Include Guards ### 4. Include Guards
@ -1181,14 +1079,13 @@ FEND
// Memory // Memory
value = PEEK $D020 value = PEEK $D020
POKE $D020, 5 POKE $D020 WITH 5
address = PEEKW $FFFC address = PEEKW $FFFC
POKEW $0314, handler POKEW $0314 WITH handler
// Operators // Operators
+ - // Arithmetic (runtime + constants) + - * / // Arithmetic
* / // Multiply/Divide (constant expressions only) & | ^ // Bitwise
& | ^ << >> // Bitwise / shift
++ -- // Increment/Decrement ++ -- // Increment/Decrement
== != < > <= >= // Comparison == != < > <= >= // Comparison

38
main.go
View file

@ -71,15 +71,11 @@ 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++ {
arg := args[i] arg := args[i]
if arg == "-i" || arg == "-in" { if arg == "-i" || arg == "-in" {
if i+1 < len(args) { if i+1 < len(args) {
inputFile = args[i+1] inputFile = args[i+1]
@ -98,45 +94,34 @@ 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
} }
} }
if inputFile == "" { if inputFile == "" {
fmt.Fprintln(os.Stderr, "Error: No input file specified") fmt.Fprintln(os.Stderr, "Error: No input file specified")
printUsage() printUsage()
os.Exit(1) os.Exit(1)
} }
// Default output based on input // Default output based on input
if outputFile == "" { if outputFile == "" {
base := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile)) base := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile))
outputFile = base + ".prg" // Default to build mode (.prg) outputFile = base + ".prg" // Default to build mode (.prg)
} }
// 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, opt, optDebug, optC64, optExcludes); err != nil { if err := build(inputFile, outputFile, false, false, false, false, false, nil); 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, opt, optDebug, optC64, optExcludes); err != nil { if err := compileOnly(inputFile, outputFile, false, false, false, nil); err != nil {
handleError(err) handleError(err)
} }
fmt.Println("Compilation successful.") fmt.Println("Compilation successful.")
@ -154,15 +139,6 @@ func compileOnly(inFile, outFile string, opt, optDebug, optC64 bool, optExcludes
// Create compiler and register commands // Create compiler and register commands
comp := compiler.NewCompiler(pragma) comp := compiler.NewCompiler(pragma)
absInput, err := filepath.Abs(inFile)
if err != nil {
return fmt.Errorf("failed to resolve input file path: %w", err)
}
projectRoot := filepath.Dir(absInput)
if canonicalRoot, err := filepath.EvalSymlinks(projectRoot); err == nil {
projectRoot = canonicalRoot
}
comp.Context().ProjectRoot = projectRoot
comp.CmdlineOpt = opt comp.CmdlineOpt = opt
comp.CmdlineDebug = optDebug comp.CmdlineDebug = optDebug
// Build I/O regions from CLI flags // Build I/O regions from CLI flags

View file

@ -12,15 +12,6 @@
"build": { "build": {
"model": "deepseek/deepseek-v4-flash", "model": "deepseek/deepseek-v4-flash",
"description": "Implementation and coding using DeepSeek V4 Flash" "description": "Implementation and coding using DeepSeek V4 Flash"
},
"build-pro": {
"model": "deepseek/deepseek-v4-pro",
"mode": "primary",
"options": {
"thinking": { "type": "enabled" },
"reasoningEffort": "high"
},
"description": "Implementation and coding using DeepSeek V4 Pro"
} }
} }
} }

122
syntax.md
View file

@ -12,7 +12,6 @@ C65GM uses C-style line comments.
**Examples:** **Examples:**
``` ```
BYTE counter = 0 // Initialize counter BYTE counter = 0 // Initialize counter
BYTE i // Loop variable (must be declared)
// This is a full line comment // This is a full line comment
FOR i = 0 TO 10 // Loop through values FOR i = 0 TO 10 // Loop through values
counter++ // Increment counter++ // Increment
@ -395,56 +394,9 @@ ENDSCRIPT
- Output from `print()` goes directly to assembler - Output from `print()` goes directly to assembler
- Can reference compiler variables using `|varname|` syntax - Can reference compiler variables using `|varname|` syntax
- Math module available: `import math` - Math module available: `import math`
- Maximum 1 million execution steps per block (prevents infinite loops). Each `SCRIPT`, `SCRIPT LIBRARY`, and `@macro()` invocation gets its own step counter. The limit is not cumulative across the compilation. - Maximum 1 million execution steps (prevents infinite loops)
- If a block exceeds the limit, compilation fails with: `Starlark computation cancelled: too many steps`
- Use `#PRAGMA _P_SCRIPT_MAX_STEPS <n>` to change the limit (e.g., `#PRAGMA _P_SCRIPT_MAX_STEPS 5000000` for more steps, or `#PRAGMA _P_SCRIPT_MAX_STEPS 100` for a tight limit during debugging). The pragma is sticky — it applies to all subsequent blocks until changed.
- Executed at compile time, not runtime - Executed at compile time, not runtime
**File I/O Built-in Functions:**
Scripts can read files from the project folder (the directory containing the main input .c65 file) using the following built-in functions:
- **`load_binary(path, offset=0, length=0)`** — Loads a binary file and returns a list of integers (0-255). Optional `offset` specifies a starting byte position, and `length` limits the number of bytes read (0 = read all remaining bytes).
- **`load_text(path)`** — Loads a text file and returns a list of strings, one per line. Handles both `\n` and `\r\n` line endings.
**Security restrictions:**
- Only relative paths are allowed (no absolute paths like `/etc/passwd`)
- Path traversal (`..`) is not permitted
- Only files inside the project folder (or its subdirectories) can be accessed
- File access errors cause a compile error with source location
**Example — loading sprite data:**
```
SCRIPT
sprite = load_binary("assets/hero.spr") # load binary data
print("hero_sprite:")
for i in range(0, len(sprite), 8):
row = ", ".join(["$%02x" % b for b in sprite[i:i+8]])
print(" !byte " + row)
ENDSCRIPT
```
**Example — loading a level map:**
```
SCRIPT LIBRARY
# Load a text-based level map at compile time
level = load_text("levels/lvl1.txt")
height = len(level)
width = len(level[0]) if height > 0 else 0
def emit_map():
print("level_map:")
for y in range(height):
print(" !text " + repr(level[y]))
print("level_height:")
print(" !8 %d" % height)
print("level_width:")
print(" !8 %d" % width)
ENDSCRIPT
```
--- ---
### SCRIPT LIBRARY...ENDSCRIPT ### SCRIPT LIBRARY...ENDSCRIPT
@ -624,26 +576,15 @@ $00
### Operators ### Operators
Evaluated strictly left to right (no precedence): Evaluated strictly left to right:
- `+` Addition (runtime and constant expressions) - `+` Addition
- `-` Subtraction (runtime and constant expressions) - `-` Subtraction
- `*` Multiplication (constant expressions only) - `*` Multiplication
- `/` Division (constant expressions only) - `/` Division
- `|` Bitwise OR - `|` Bitwise OR
- `&` Bitwise AND - `&` Bitwise AND
- `^` Bitwise XOR - `^` Bitwise XOR
- `<<` Shift left
- `>>` Shift right
There are two distinct expression forms:
- **Constant expressions** — written *without spaces* (e.g. `2+3*4`). Every
term must be a literal or a `CONST`; these are folded at compile time and
may chain any number of terms. `*` and `/` are only available here.
- **Runtime expressions** — written *with spaces* (e.g. `a + b`). These
perform exactly one operation, where at least one operand may be a variable.
Only `+ - & | ^ << >>` are supported; `*` and `/` are not.
### Constants ### Constants
@ -659,20 +600,13 @@ pointer = SCREEN
### Expression Examples ### Expression Examples
Constant expressions (no spaces, all terms literals or constants):
``` ```
value = 100+50 value = 100+50
result = $FF-10 result = $FF-10
address = $D000+32 address = $D000+32
mask = %11110000&$0F mask = %11110000&$0F
combined = BASE|OFFSET combined = base|offset
calculated = START+LENGTH*2 calculated = start+length*2
```
Runtime expressions (spaces, one operation, may use variables):
```
combined = base | offset
adjusted = value + count
``` ```
**Critical:** No operator precedence. Evaluation is strictly left to right: **Critical:** No operator precedence. Evaluation is strictly left to right:
@ -701,22 +635,15 @@ INC $D000+20
``` ```
FOR i = 0 TO MAX_VALUE-1 FOR i = 0 TO MAX_VALUE-1
IF x > THRESHOLD+10 IF x > THRESHOLD+10
POKE $D020+OFFSET, value POKE $D020+offset WITH value
result = PEEK $0400+INDEX result = PEEK $0400+index
``` ```
**Arithmetic operations:** **Arithmetic operations:**
Constant expressions (compile-time, all terms literal/CONST):
``` ```
sum = VALUE1+VALUE2 sum = value1+value2
adjusted = BASE+OFFSET product = base*factor
``` adjusted = original+OFFSET
Runtime expressions (one operation, may use variables):
```
sum = value1 + value2
adjusted = original + offset
``` ```
### Limitations ### Limitations
@ -728,15 +655,11 @@ result = (a+b)*c
value = base+(offset*2) value = base+(offset*2)
``` ```
**No multi-operation runtime expressions:** **No nested expressions in assignments:**
``` ```
; NOT SUPPORTED (more than one operation with variables): ; NOT SUPPORTED:
x = a + b + c ; chain of runtime operations x = y + z ; only single value or constant expression
x = a + b * c ; ditto x = a+b ; constant expression (no spaces) OK if a,b are constants
; SUPPORTED:
x = y + z ; single runtime operation (one operator)
x = a+b ; constant expression (no spaces) if a,b are literals/CONST
``` ```
**Workaround for complex expressions:** **Workaround for complex expressions:**
@ -750,10 +673,7 @@ result = temp - c
Expressions without spaces are treated as constant expressions: Expressions without spaces are treated as constant expressions:
``` ```
value = 100+50 ; constant expression (folded at compile time) value = 100+50 ; OK - constant expression
value = 100 + 50 ; single runtime operation (also OK) value = 100 + 50 ; ERROR - not a simple assignment
value = MAX+10 ; constant expression, OK if MAX is a constant value = MAX+10 ; OK if MAX is constant
``` ```
The difference matters for chaining: `2+3*4` (no spaces) folds all terms at
compile time, while a spaced form allows only one operation.