Compare commits
No commits in common. "main" and "peephole_optimizer" have entirely different histories.
main
...
peephole_o
73 changed files with 506 additions and 6136 deletions
2
BUILDNUM
2
BUILDNUM
|
|
@ -1 +1 @@
|
||||||
6
|
5
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
35
commands.md
35
commands.md
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
x64 -autostartprgmode 1 irq_demo.prg
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
!"#$%&'(
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
x64 -autostartprgmode 1 load_binary_demo.prg
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ FUNC wait_key
|
||||||
WEND
|
WEND
|
||||||
|
|
||||||
// Reset key buffer
|
// Reset key buffer
|
||||||
POKE $c6, 0
|
POKE $c6 WITH 0
|
||||||
|
|
||||||
FEND
|
FEND
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ FUNC wait_key
|
||||||
ENDIF
|
ENDIF
|
||||||
WEND
|
WEND
|
||||||
|
|
||||||
POKE $c6, 0
|
POKE $c6 WITH 0
|
||||||
FEND
|
FEND
|
||||||
|
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ FUNC wait_key
|
||||||
ENDIF
|
ENDIF
|
||||||
WEND
|
WEND
|
||||||
|
|
||||||
POKE $c6, 0
|
POKE $c6 WITH 0
|
||||||
FEND
|
FEND
|
||||||
|
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
x64 -autostartprgmode 1 multicolorbm_v2.prg
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: $")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,24 +96,24 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
// 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -334,7 +270,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma()
|
funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma()
|
||||||
|
|
||||||
// Check for unused variables and print warnings (skip variables in functions with remove pragma)
|
// Check for unused variables and print warnings (skip variables in functions with remove pragma)
|
||||||
warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma, 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, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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, ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
219
language.md
219
language.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
28
main.go
28
main.go
|
|
@ -71,10 +71,6 @@ 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++ {
|
||||||
|
|
@ -98,17 +94,6 @@ 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
|
||||||
|
|
@ -130,13 +115,13 @@ func main() {
|
||||||
// Determine mode by output extension
|
// Determine mode by output extension
|
||||||
if strings.HasSuffix(strings.ToLower(outputFile), ".prg") {
|
if strings.HasSuffix(strings.ToLower(outputFile), ".prg") {
|
||||||
// Build mode (compile + assemble)
|
// Build mode (compile + assemble)
|
||||||
if err := build(inputFile, outputFile, false, false, 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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
120
syntax.md
120
syntax.md
|
|
@ -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.
|
|
||||||
Loading…
Reference in a new issue