Compare commits
9 commits
peephole_o
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d94dde5ea | |||
| cf01db5e28 | |||
| a1c9e16abe | |||
| 9f24408a0b | |||
| b2e9e3dc17 | |||
| 3748e0bd3e | |||
| d4f1213c06 | |||
| 205ae92d0e | |||
| c76c514d35 |
38 changed files with 3976 additions and 172 deletions
2
BUILDNUM
2
BUILDNUM
|
|
@ -1 +1 @@
|
|||
5
|
||||
6
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
FROM ghcr.io/anomalyco/opencode:1.14.48
|
||||
#FROM ghcr.io/anomalyco/opencode:latest
|
||||
#FROM ghcr.io/anomalyco/opencode:1.14.48
|
||||
FROM ghcr.io/anomalyco/opencode:latest
|
||||
|
||||
RUN apk add --no-cache go gcc musl-dev
|
||||
|
||||
|
|
|
|||
5
examples/load_binary_demo/cm.sh
Executable file
5
examples/load_binary_demo/cm.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
# Define filename as variable
|
||||
PROGNAME="load_binary_demo"
|
||||
# Compile and assemble directly
|
||||
c65gm ${PROGNAME}.c65
|
||||
50
examples/load_binary_demo/load_binary_demo.c65
Normal file
50
examples/load_binary_demo/load_binary_demo.c65
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//-----------------------------------------------------------
|
||||
// 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
|
||||
2
examples/load_binary_demo/screendata.bin
Normal file
2
examples/load_binary_demo/screendata.bin
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
!"#$%&'(
|
||||
1
examples/load_binary_demo/start_in_vice.sh
Normal file
1
examples/load_binary_demo/start_in_vice.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
x64 -autostartprgmode 1 load_binary_demo.prg
|
||||
|
|
@ -40,7 +40,7 @@ FUNC wait_key
|
|||
WEND
|
||||
|
||||
// Reset key buffer
|
||||
POKE $c6 WITH 0
|
||||
POKE $c6, 0
|
||||
|
||||
FEND
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ FUNC wait_key
|
|||
ENDIF
|
||||
WEND
|
||||
|
||||
POKE $c6 WITH 0
|
||||
POKE $c6, 0
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ FUNC wait_key
|
|||
ENDIF
|
||||
WEND
|
||||
|
||||
POKE $c6 WITH 0
|
||||
POKE $c6, 0
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
# Define filename as variable
|
||||
PROGNAME="multicolorbm"
|
||||
# Compile and assemble directly
|
||||
c65gm ${PROGNAME}.c65
|
||||
# Compile and assemble directly, keep intermediate .asm file, enable optimizations
|
||||
c65gm build -i ${PROGNAME}.c65 --keep-asm --opt
|
||||
5
examples/multicolorbm_v2/cm.sh
Executable file
5
examples/multicolorbm_v2/cm.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/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
|
||||
131
examples/multicolorbm_v2/multicolorbm_v2.c65
Normal file
131
examples/multicolorbm_v2/multicolorbm_v2.c65
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
//-----------------------------------------------------------
|
||||
// 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 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
examples/multicolorbm_v2/start_in_vice.sh
Executable file
1
examples/multicolorbm_v2/start_in_vice.sh
Executable file
|
|
@ -0,0 +1 @@
|
|||
x64 -autostartprgmode 1 multicolorbm_v2.prg
|
||||
|
|
@ -43,7 +43,7 @@ FUNC wait_key
|
|||
WEND
|
||||
|
||||
// Reset key buffer
|
||||
POKE $c6 WITH 0
|
||||
POKE $c6, 0
|
||||
|
||||
FEND
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,26 @@ 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
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +200,46 @@ func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
}
|
||||
|
||||
// At least one param is a variable - generate AND code
|
||||
|
||||
// Same variable on both sides: a & a = a
|
||||
if c.param1IsVar && c.param2IsVar && c.param1VarName == c.param2VarName {
|
||||
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 {
|
||||
// Load param1
|
||||
if c.param1IsVar {
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
|
||||
|
|
@ -187,18 +247,36 @@ func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
|
||||
}
|
||||
|
||||
// AND with param2
|
||||
// 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 {
|
||||
} 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))
|
||||
}
|
||||
|
||||
// If destination is word, handle high byte
|
||||
if c.destVarKind == compiler.KindWord {
|
||||
// Determine if param2 high byte is effectively 0
|
||||
// (AND with 0 always yields 0 regardless of param1 high byte)
|
||||
param2HiEffectiveZero := false
|
||||
if c.param2IsVar {
|
||||
if c.param2VarKind == compiler.KindByte {
|
||||
param2HiEffectiveZero = true
|
||||
}
|
||||
} else {
|
||||
param2HiEffectiveZero = ((c.param2Value >> 8) & 0xFF) == 0
|
||||
}
|
||||
|
||||
if param2HiEffectiveZero {
|
||||
if !aIsZero {
|
||||
asm = append(asm, "\tlda #0")
|
||||
}
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
} else {
|
||||
// Load high byte of param1
|
||||
if c.param1IsVar {
|
||||
if c.param1VarKind == compiler.KindWord {
|
||||
|
|
@ -216,7 +294,7 @@ func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
if c.param2VarKind == compiler.KindWord {
|
||||
asm = append(asm, fmt.Sprintf("\tand %s+1", c.param2VarName))
|
||||
} else {
|
||||
asm = append(asm, "\tand #0")
|
||||
asm = append(asm, fmt.Sprintf("\tand #$%02x", uint8(c.param2Value&0xFF)))
|
||||
}
|
||||
} else {
|
||||
hi := uint8((c.param2Value >> 8) & 0xFF)
|
||||
|
|
@ -226,6 +304,7 @@ func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
// Store high byte
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
}
|
||||
|
||||
return asm, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ func TestAndCommand_OldSyntax(t *testing.T) {
|
|||
"\tand b",
|
||||
"\tsta result",
|
||||
"\tlda #0",
|
||||
"\tand #0",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
|
|
@ -113,8 +112,7 @@ func TestAndCommand_OldSyntax(t *testing.T) {
|
|||
st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
wantAsm: []string{
|
||||
"\tlda #$ff",
|
||||
"\tand b",
|
||||
"\tlda b",
|
||||
"\tsta result",
|
||||
},
|
||||
},
|
||||
|
|
@ -182,8 +180,7 @@ func TestAndCommand_OldSyntax(t *testing.T) {
|
|||
"\tlda wval",
|
||||
"\tand bval",
|
||||
"\tsta result",
|
||||
"\tlda wval+1",
|
||||
"\tand #0",
|
||||
"\tlda #0",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
|
|
@ -286,7 +283,6 @@ func TestAndCommand_NewSyntax(t *testing.T) {
|
|||
"\tand b",
|
||||
"\tsta result",
|
||||
"\tlda #0",
|
||||
"\tand #0",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
|
|
@ -320,6 +316,96 @@ func TestAndCommand_NewSyntax(t *testing.T) {
|
|||
"\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",
|
||||
line: "result = 255 & 15",
|
||||
|
|
@ -358,6 +444,206 @@ func TestAndCommand_NewSyntax(t *testing.T) {
|
|||
"\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",
|
||||
line: "unknown = a & b",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
type MacroCommand struct {
|
||||
macroName string
|
||||
args []string
|
||||
pragmaSetIndex int
|
||||
}
|
||||
|
||||
func (c *MacroCommand) WillHandle(line preproc.Line) bool {
|
||||
|
|
@ -30,11 +31,12 @@ func (c *MacroCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext)
|
|||
|
||||
c.macroName = name
|
||||
c.args = args
|
||||
c.pragmaSetIndex = line.PragmaSetIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||
macroOutput, err := compiler.ExecuteMacro(c.macroName, c.args, ctx)
|
||||
macroOutput, err := compiler.ExecuteMacro(c.macroName, c.args, ctx, c.pragmaSetIndex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("macro %s: %w", c.macroName, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,26 @@ 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
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +200,39 @@ func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
}
|
||||
|
||||
// At least one param is a variable - generate OR code
|
||||
|
||||
// Same variable on both sides: a | a = a
|
||||
if c.param1IsVar && c.param2IsVar && c.param1VarName == c.param2VarName {
|
||||
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 {
|
||||
// Load param1
|
||||
if c.param1IsVar {
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
|
||||
|
|
@ -187,18 +240,33 @@ func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
|
||||
}
|
||||
|
||||
// OR with param2
|
||||
// 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 {
|
||||
} 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))
|
||||
}
|
||||
|
||||
// If destination is word, handle high byte
|
||||
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
|
||||
if c.param1IsVar {
|
||||
if c.param1VarKind == compiler.KindWord {
|
||||
|
|
@ -211,17 +279,19 @@ func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
|
||||
}
|
||||
|
||||
// OR with high byte of param2
|
||||
// OR with high byte of param2 (skip if literal 0, as ora #0 is a no-op)
|
||||
if c.param2IsVar {
|
||||
if c.param2VarKind == compiler.KindWord {
|
||||
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 {
|
||||
hi := uint8((c.param2Value >> 8) & 0xFF)
|
||||
if hi != 0 {
|
||||
asm = append(asm, fmt.Sprintf("\tora #$%02x", hi))
|
||||
}
|
||||
// Skip ora when param2 const high byte is 0
|
||||
}
|
||||
|
||||
// Store high byte
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ func TestOrCommand_OldSyntax(t *testing.T) {
|
|||
"\tora b",
|
||||
"\tsta result",
|
||||
"\tlda #0",
|
||||
"\tora #0",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
|
|
@ -183,7 +182,6 @@ func TestOrCommand_OldSyntax(t *testing.T) {
|
|||
"\tora bval",
|
||||
"\tsta result",
|
||||
"\tlda wval+1",
|
||||
"\tora #0",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
|
|
@ -286,7 +284,6 @@ func TestOrCommand_NewSyntax(t *testing.T) {
|
|||
"\tora b",
|
||||
"\tsta result",
|
||||
"\tlda #0",
|
||||
"\tora #0",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
|
|
@ -320,6 +317,32 @@ func TestOrCommand_NewSyntax(t *testing.T) {
|
|||
"\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",
|
||||
line: "result = 15 | 240",
|
||||
|
|
@ -358,6 +381,294 @@ func TestOrCommand_NewSyntax(t *testing.T) {
|
|||
"\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",
|
||||
line: "unknown = a | b",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type PokeCommand struct {
|
|||
}
|
||||
|
||||
func (c *PokeCommand) WillHandle(line preproc.Line) bool {
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text))
|
||||
if err != nil || len(params) != 4 {
|
||||
return false
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ func (c *PokeCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
|
|||
// Store pragma set for Generate phase
|
||||
c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
|
||||
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
987
internal/commands/poke_test.go
Normal file
987
internal/commands/poke_test.go
Normal file
|
|
@ -0,0 +1,987 @@
|
|||
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 {
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text))
|
||||
if err != nil || len(params) != 4 {
|
||||
return false
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ func (c *PokeWCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContex
|
|||
// Store pragma set for Generate phase
|
||||
c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
|
||||
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
params, err := utils.ParseParams(utils.NormalizeCommas(line.Text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,21 @@ func (c *XorCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
}
|
||||
|
||||
// At least one param is a variable - generate XOR code
|
||||
|
||||
// Same variable on both sides: a ^ a = 0
|
||||
if c.param1IsVar && c.param2IsVar && c.param1VarName == c.param2VarName {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
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)
|
||||
if !c.param1IsVar && uint8(c.param1Value&0xFF) == 0 && c.param2IsVar {
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.param2VarName))
|
||||
} else {
|
||||
// Load param1
|
||||
if c.param1IsVar {
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
|
||||
|
|
@ -207,12 +222,13 @@ func (c *XorCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
|
|||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
|
||||
}
|
||||
|
||||
// XOR with param2
|
||||
// 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 {
|
||||
} else if uint8(c.param2Value&0xFF) != 0 {
|
||||
asm = append(asm, fmt.Sprintf("\teor #$%02x", uint8(c.param2Value&0xFF)))
|
||||
}
|
||||
}
|
||||
|
||||
// Store low byte
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
|
|
|
|||
|
|
@ -463,6 +463,44 @@ func TestXorCommand_NewSyntax(t *testing.T) {
|
|||
"\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",
|
||||
line: "result = 255 ^ 170",
|
||||
|
|
@ -501,6 +539,31 @@ func TestXorCommand_NewSyntax(t *testing.T) {
|
|||
"\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",
|
||||
line: "unknown = a ^ b",
|
||||
|
|
|
|||
|
|
@ -42,11 +42,13 @@ func (c *Compiler) Registry() *CommandRegistry {
|
|||
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||
var codeOutput []string
|
||||
var lastKind = preproc.Source
|
||||
var scriptBuffer []string
|
||||
var scriptBuffer []preproc.Line
|
||||
var scriptIsLibrary bool
|
||||
var macroBuffer []string
|
||||
var currentMacroName string
|
||||
var currentMacroParams []string
|
||||
var currentMacroSourceFile string
|
||||
var currentMacroStartLine int
|
||||
var currentAsmTarget *[]string // nil = no active ASM block, or points to target slice
|
||||
|
||||
// Reset deferred ASM storage for this compilation
|
||||
|
|
@ -77,12 +79,16 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
Name: currentMacroName,
|
||||
Params: currentMacroParams,
|
||||
Body: macroBuffer,
|
||||
SourceFile: currentMacroSourceFile,
|
||||
StartLine: currentMacroStartLine,
|
||||
}
|
||||
codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
|
||||
}
|
||||
macroBuffer = nil
|
||||
currentMacroName = ""
|
||||
currentMacroParams = nil
|
||||
currentMacroSourceFile = ""
|
||||
currentMacroStartLine = 0
|
||||
}
|
||||
|
||||
// Close previous Assembler block
|
||||
|
|
@ -163,7 +169,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
return nil, fmt.Errorf("compilation failed")
|
||||
}
|
||||
|
||||
macroOutput, err := ExecuteMacro(macroName, args, c.ctx)
|
||||
macroOutput, err := ExecuteMacro(macroName, args, c.ctx, line.PragmaSetIndex)
|
||||
if err != nil {
|
||||
c.printErrorWithContext(lines, i, fmt.Errorf("macro %s: %w", macroName, err))
|
||||
return nil, fmt.Errorf("compilation failed")
|
||||
|
|
@ -199,12 +205,17 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
*currentAsmTarget = append(*currentAsmTarget, codePart+commentPart)
|
||||
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
||||
// Collect script lines for execution
|
||||
scriptBuffer = append(scriptBuffer, line.Text)
|
||||
scriptBuffer = append(scriptBuffer, line)
|
||||
} else if line.Kind == preproc.ScriptMacroDef {
|
||||
// Skip the header line (already parsed in transition)
|
||||
if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") {
|
||||
continue
|
||||
}
|
||||
// Capture source provenance from first body line
|
||||
if len(macroBuffer) == 0 {
|
||||
currentMacroSourceFile = line.Filename
|
||||
currentMacroStartLine = line.LineNo
|
||||
}
|
||||
// Collect macro body lines
|
||||
macroBuffer = append(macroBuffer, line.Text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ func TestExecuteScript_BasicPrint(t *testing.T) {
|
|||
" print(' nop')",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ func TestExecuteScript_EmptyOutput(t *testing.T) {
|
|||
"x = 1 + 1",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ func TestExecuteScript_Library_DefinesFunction(t *testing.T) {
|
|||
" print(' nop')",
|
||||
}
|
||||
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
|
|||
" print(' nop')",
|
||||
}
|
||||
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
|
|||
"emit_nops(2)",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("script executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -267,7 +267,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
|||
"def func_a():",
|
||||
" print(' ; from a')",
|
||||
}
|
||||
_, err := executeScript(lib1, ctx, true)
|
||||
_, err := testExecuteScript(lib1, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("lib1 failed: %v", err)
|
||||
}
|
||||
|
|
@ -277,7 +277,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
|||
"def func_b():",
|
||||
" print(' ; from b')",
|
||||
}
|
||||
_, err = executeScript(lib2, ctx, true)
|
||||
_, err = testExecuteScript(lib2, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("lib2 failed: %v", err)
|
||||
}
|
||||
|
|
@ -295,7 +295,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
|||
"func_a()",
|
||||
"func_b()",
|
||||
}
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("script failed: %v", err)
|
||||
}
|
||||
|
|
@ -322,7 +322,7 @@ func TestExecuteScript_RegularScript_DoesNotPersist(t *testing.T) {
|
|||
"local_func()",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("script failed: %v", err)
|
||||
}
|
||||
|
|
@ -352,7 +352,7 @@ func TestExecuteMacro_Basic(t *testing.T) {
|
|||
}
|
||||
|
||||
// Execute macro
|
||||
output, err := ExecuteMacro("test_macro", []string{"3"}, ctx)
|
||||
output, err := ExecuteMacro("test_macro", []string{"3"}, ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteMacro failed: %v", err)
|
||||
}
|
||||
|
|
@ -377,7 +377,7 @@ func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
|
|||
"def emit_nop():",
|
||||
" print(' nop')",
|
||||
}
|
||||
_, err := executeScript(lib, ctx, true)
|
||||
_, err := testExecuteScript(lib, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library failed: %v", err)
|
||||
}
|
||||
|
|
@ -392,7 +392,7 @@ func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
|
|||
}
|
||||
|
||||
// Execute macro
|
||||
output, err := ExecuteMacro("nop_macro", []string{}, ctx)
|
||||
output, err := ExecuteMacro("nop_macro", []string{}, ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteMacro failed: %v", err)
|
||||
}
|
||||
|
|
@ -416,7 +416,7 @@ func TestExecuteMacro_StringParameter(t *testing.T) {
|
|||
}
|
||||
|
||||
// Execute with identifier (should be passed as string)
|
||||
output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx)
|
||||
output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx, 0)
|
||||
if err != nil {
|
||||
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
|
||||
output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx)
|
||||
output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx, 0)
|
||||
if err != nil {
|
||||
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
|
||||
output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx)
|
||||
output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteMacro failed: %v", err)
|
||||
}
|
||||
|
|
@ -540,7 +540,7 @@ func TestExecuteScript_LocalVariableExpansion(t *testing.T) {
|
|||
"print(' inc |counter|')",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -569,7 +569,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
|
|||
" print(' inc |global_counter|')",
|
||||
}
|
||||
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library script failed: %v", err)
|
||||
}
|
||||
|
|
@ -579,7 +579,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
|
|||
"inc_global()",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
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
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library script failed: %v", err)
|
||||
}
|
||||
|
|
@ -625,7 +625,7 @@ func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
|
|||
"use_local()",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -1219,3 +1219,96 @@ func TestAsmAfterVarsWithVariables(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ type ScriptMacro struct {
|
|||
Name string // macro name
|
||||
Params []string // parameter names
|
||||
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
|
||||
|
|
@ -41,6 +43,11 @@ type CompilerContext struct {
|
|||
|
||||
// ScriptMacros holds named macro definitions from SCRIPT MACRO blocks
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,29 +2,229 @@ package compiler
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"c65gm/internal/preproc"
|
||||
"c65gm/internal/utils"
|
||||
|
||||
"go.starlark.net/lib/math"
|
||||
"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.
|
||||
// If isLibrary is true, the script is executed at top level (no _main wrapper)
|
||||
// and resulting globals are persisted to ctx.ScriptLibraryGlobals.
|
||||
func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
|
||||
func executeScript(scriptLines []preproc.Line, 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
|
||||
scriptText := strings.Join(scriptLines, "\n")
|
||||
scriptText := strings.Join(texts, "\n")
|
||||
|
||||
// Expand |varname| -> actual variable names
|
||||
scriptText = expandVariables(scriptText, ctx)
|
||||
|
||||
// Determine the source filename for Starlark
|
||||
sourceFile := scriptLines[0].Filename
|
||||
|
||||
var finalScript string
|
||||
var starlarkFilename string
|
||||
if isLibrary {
|
||||
// LIBRARY: execute at top level so defs become globals
|
||||
finalScript = scriptText
|
||||
starlarkFilename = sourceFile
|
||||
} else {
|
||||
// Regular SCRIPT: wrap in function (Starlark requires control flow inside functions)
|
||||
finalScript = "def _main():\n"
|
||||
|
|
@ -32,6 +232,7 @@ func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) (
|
|||
finalScript += " " + line + "\n"
|
||||
}
|
||||
finalScript += "_main()\n"
|
||||
starlarkFilename = sourceFile
|
||||
}
|
||||
|
||||
// Capture print output
|
||||
|
|
@ -43,21 +244,41 @@ func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) (
|
|||
},
|
||||
}
|
||||
|
||||
// Set execution limit (prevent infinite loops)
|
||||
thread.SetMaxExecutionSteps(1000000) // 1M steps
|
||||
// Set execution limit from pragma or default (prevent infinite loops)
|
||||
thread.SetMaxExecutionSteps(readScriptMaxSteps(ctx, scriptLines[0].PragmaSetIndex))
|
||||
|
||||
// Build predeclared: math module + library globals
|
||||
// Build predeclared: math module + library globals + file I/O builtins
|
||||
predeclared := starlark.StringDict{
|
||||
"math": math.Module,
|
||||
"load_binary": makeLoadBinary(ctx.ProjectRoot),
|
||||
"load_text": makeLoadText(ctx.ProjectRoot),
|
||||
}
|
||||
for k, v := range ctx.ScriptLibraryGlobals {
|
||||
predeclared[k] = v
|
||||
}
|
||||
|
||||
// Execute
|
||||
globals, err := starlark.ExecFile(thread, "script.star", finalScript, predeclared)
|
||||
globals, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Map Starlark error position back to source
|
||||
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)
|
||||
|
|
@ -97,8 +318,149 @@ func expandVariables(text string, ctx *CompilerContext) string {
|
|||
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
|
||||
func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]string, error) {
|
||||
// pragmaSetIndex is the index of the pragma set at the macro invocation call site.
|
||||
func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaSetIndex int) ([]string, error) {
|
||||
// Look up the macro
|
||||
macro, ok := ctx.ScriptMacros[macroName]
|
||||
if !ok {
|
||||
|
|
@ -130,6 +492,12 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
|||
}
|
||||
finalScript += "_macro()\n"
|
||||
|
||||
// Use the source file where the macro was defined
|
||||
starlarkFilename := macro.SourceFile
|
||||
if starlarkFilename == "" {
|
||||
starlarkFilename = "macro.star"
|
||||
}
|
||||
|
||||
// Capture print output
|
||||
var output bytes.Buffer
|
||||
thread := &starlark.Thread{
|
||||
|
|
@ -139,12 +507,14 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
|||
},
|
||||
}
|
||||
|
||||
// Set execution limit
|
||||
thread.SetMaxExecutionSteps(1000000)
|
||||
// Set execution limit from pragma at call site or default
|
||||
thread.SetMaxExecutionSteps(readScriptMaxSteps(ctx, pragmaSetIndex))
|
||||
|
||||
// Build predeclared: math + library globals + parameter bindings
|
||||
// Build predeclared: math + library globals + file I/O builtins + parameter bindings
|
||||
predeclared := starlark.StringDict{
|
||||
"math": math.Module,
|
||||
"load_binary": makeLoadBinary(ctx.ProjectRoot),
|
||||
"load_text": makeLoadText(ctx.ProjectRoot),
|
||||
}
|
||||
for k, v := range ctx.ScriptLibraryGlobals {
|
||||
predeclared[k] = v
|
||||
|
|
@ -154,9 +524,19 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
|||
}
|
||||
|
||||
// Execute
|
||||
_, err := starlark.ExecFile(thread, "macro.star", finalScript, predeclared)
|
||||
_, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Map error position back to macro definition site
|
||||
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
|
||||
|
|
|
|||
1097
internal/compiler/scriptexec_test.go
Normal file
1097
internal/compiler/scriptexec_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -170,7 +170,21 @@ func (p *preproc) run(root string) ([]Line, error) {
|
|||
p.inScript = false
|
||||
p.inScriptLibrary = false
|
||||
p.inScriptMacro = false
|
||||
continue // don't emit ENDSCRIPT marker
|
||||
// Emit an empty Source line as a block boundary marker so the
|
||||
// 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
|
||||
kind := Script
|
||||
|
|
|
|||
|
|
@ -151,9 +151,10 @@ func TestPreProcess_ScriptBlock(t *testing.T) {
|
|||
t.Fatalf("PreProcess failed: %v", err)
|
||||
}
|
||||
|
||||
// SCRIPT and ENDSCRIPT markers are stripped
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("expected 3 lines, got %d", len(lines))
|
||||
// SCRIPT and ENDSCRIPT markers are stripped, but ENDSCRIPT emits
|
||||
// an empty Source boundary marker
|
||||
if len(lines) != 4 {
|
||||
t.Fatalf("expected 4 lines, got %d", len(lines))
|
||||
}
|
||||
|
||||
// Script content should NOT be processed
|
||||
|
|
@ -171,12 +172,20 @@ func TestPreProcess_ScriptBlock(t *testing.T) {
|
|||
t.Errorf("expected Kind=Script, got %v", lines[1].Kind)
|
||||
}
|
||||
|
||||
// After ENDSCRIPT, defines work again
|
||||
if lines[2].Text != "LDA #100" {
|
||||
t.Errorf("expected 'LDA #100', got %q", lines[2].Text)
|
||||
}
|
||||
// Line 2 is the ENDSCRIPT boundary marker (empty Source line)
|
||||
if lines[2].Kind != Source {
|
||||
t.Errorf("expected Kind=Source, got %v", lines[2].Kind)
|
||||
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
|
||||
if lines[3].Text != "LDA #100" {
|
||||
t.Errorf("expected 'LDA #100', got %q", lines[3].Text)
|
||||
}
|
||||
if lines[3].Kind != Source {
|
||||
t.Errorf("expected Kind=Source, got %v", lines[3].Kind)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,8 +271,9 @@ func TestPreProcess_CommentInScriptBlock(t *testing.T) {
|
|||
t.Fatalf("PreProcess failed: %v", err)
|
||||
}
|
||||
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("expected 2 lines, got %d", len(lines))
|
||||
// 2 script lines + 1 ENDSCRIPT boundary marker + 0 trailing source = 3 lines
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("expected 3 lines, got %d", len(lines))
|
||||
}
|
||||
|
||||
// Comments should be preserved in Script blocks
|
||||
|
|
@ -273,6 +283,11 @@ func TestPreProcess_CommentInScriptBlock(t *testing.T) {
|
|||
if lines[1].Text != " y = 2 // another one" {
|
||||
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) {
|
||||
|
|
@ -883,6 +898,7 @@ func TestPreProcess_MixedBlocksAndComments(t *testing.T) {
|
|||
{"LDA #10", Source},
|
||||
{" lda #X // asm comment", Assembler},
|
||||
{" y = X // script comment", Script},
|
||||
{"", Source}, // ENDSCRIPT boundary marker
|
||||
{"STA $D020", Source},
|
||||
}
|
||||
|
||||
|
|
@ -938,12 +954,17 @@ func TestPreProcess_EmptyScriptBlock(t *testing.T) {
|
|||
t.Fatalf("PreProcess failed: %v", err)
|
||||
}
|
||||
|
||||
if len(lines) != 1 {
|
||||
t.Fatalf("expected 1 line, got %d", len(lines))
|
||||
// Empty script emits an ENDSCRIPT boundary marker + NOP
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("expected 2 lines, got %d", len(lines))
|
||||
}
|
||||
|
||||
if lines[0].Text != "NOP" {
|
||||
t.Errorf("expected 'NOP', got %q", lines[0].Text)
|
||||
if lines[0].Kind != Source || lines[0].Text != "" {
|
||||
t.Errorf("line 0: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[0].Kind, lines[0].Text)
|
||||
}
|
||||
|
||||
if lines[1].Text != "NOP" {
|
||||
t.Errorf("expected 'NOP', got %q", lines[1].Text)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -963,9 +984,9 @@ func TestPreProcess_ScriptLibraryBlock(t *testing.T) {
|
|||
t.Fatalf("PreProcess failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have 2 script lines + 1 source line
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("expected 3 lines, got %d", len(lines))
|
||||
// Should have 2 script lines + 1 ENDSCRIPT boundary + 1 source line = 4 lines
|
||||
if len(lines) != 4 {
|
||||
t.Fatalf("expected 4 lines, got %d", len(lines))
|
||||
}
|
||||
|
||||
// Script library lines should have ScriptLibrary kind
|
||||
|
|
@ -980,12 +1001,20 @@ func TestPreProcess_ScriptLibraryBlock(t *testing.T) {
|
|||
t.Errorf("expected Kind=ScriptLibrary, got %v", lines[1].Kind)
|
||||
}
|
||||
|
||||
// Source line after ENDSCRIPT
|
||||
// Line 2 is the ENDSCRIPT boundary marker
|
||||
if lines[2].Kind != Source {
|
||||
t.Errorf("expected Kind=Source, got %v", lines[2].Kind)
|
||||
t.Errorf("line 2: expected Kind=Source (ENDSCRIPT boundary), got %v", lines[2].Kind)
|
||||
}
|
||||
if lines[2].Text != "NOP" {
|
||||
t.Errorf("expected 'NOP', got %q", lines[2].Text)
|
||||
if lines[2].Text != "" {
|
||||
t.Errorf("line 2: expected empty text, got %q", lines[2].Text)
|
||||
}
|
||||
|
||||
// Source line after ENDSCRIPT
|
||||
if lines[3].Kind != Source {
|
||||
t.Errorf("expected Kind=Source, got %v", lines[3].Kind)
|
||||
}
|
||||
if lines[3].Text != "NOP" {
|
||||
t.Errorf("expected 'NOP', got %q", lines[3].Text)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1006,18 +1035,29 @@ func TestPreProcess_ScriptVsScriptLibrary(t *testing.T) {
|
|||
t.Fatalf("PreProcess failed: %v", err)
|
||||
}
|
||||
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("expected 2 lines, got %d", len(lines))
|
||||
// Two blocks → 1 lib line + 1 lib ENDSCRIPT boundary + 1 script line + 1 script ENDSCRIPT boundary = 4 lines
|
||||
if len(lines) != 4 {
|
||||
t.Fatalf("expected 4 lines, got %d", len(lines))
|
||||
}
|
||||
|
||||
// First line is from SCRIPT LIBRARY
|
||||
// Line 0: SCRIPT LIBRARY content
|
||||
if lines[0].Kind != ScriptLibrary {
|
||||
t.Errorf("line 0: expected ScriptLibrary, got %v", lines[0].Kind)
|
||||
}
|
||||
|
||||
// Second line is from regular SCRIPT
|
||||
if lines[1].Kind != Script {
|
||||
t.Errorf("line 1: expected Script, got %v", lines[1].Kind)
|
||||
// Line 1: ENDSCRIPT boundary for library
|
||||
if lines[1].Kind != Source || lines[1].Text != "" {
|
||||
t.Errorf("line 1: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[1].Kind, lines[1].Text)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1036,9 +1076,9 @@ func TestPreProcess_ScriptMacroBlock(t *testing.T) {
|
|||
t.Fatalf("PreProcess failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have header + body line + source line = 3 lines
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("expected 3 lines, got %d", len(lines))
|
||||
// Should have header + body line + ENDSCRIPT boundary + source line = 4 lines
|
||||
if len(lines) != 4 {
|
||||
t.Fatalf("expected 4 lines, got %d", len(lines))
|
||||
}
|
||||
|
||||
// First line is the header (also ScriptMacroDef kind)
|
||||
|
|
@ -1054,12 +1094,20 @@ func TestPreProcess_ScriptMacroBlock(t *testing.T) {
|
|||
t.Errorf("line 1: expected ScriptMacroDef, got %v", lines[1].Kind)
|
||||
}
|
||||
|
||||
// Third line is source
|
||||
// Third line is ENDSCRIPT boundary marker
|
||||
if lines[2].Kind != Source {
|
||||
t.Errorf("line 2: expected Source, got %v", lines[2].Kind)
|
||||
t.Errorf("line 2: expected Source (ENDSCRIPT boundary), got %v", lines[2].Kind)
|
||||
}
|
||||
if lines[2].Text != "NOP" {
|
||||
t.Errorf("line 2: expected 'NOP', got %q", lines[2].Text)
|
||||
if lines[2].Text != "" {
|
||||
t.Errorf("line 2: expected empty text, 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,6 +102,34 @@ func ToUpper(s string) string {
|
|||
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)
|
||||
func ValidateIdentifier(s string) bool {
|
||||
if len(s) == 0 {
|
||||
|
|
|
|||
52
language.md
52
language.md
|
|
@ -507,6 +507,58 @@ SCRIPT
|
|||
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
|
||||
|
||||
Define reusable Starlark functions that persist across all subsequent SCRIPT blocks:
|
||||
|
|
|
|||
9
main.go
9
main.go
|
|
@ -139,6 +139,15 @@ func compileOnly(inFile, outFile string, opt, optDebug, optC64 bool, optExcludes
|
|||
|
||||
// Create compiler and register commands
|
||||
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.CmdlineDebug = optDebug
|
||||
// Build I/O regions from CLI flags
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@
|
|||
"build": {
|
||||
"model": "deepseek/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"
|
||||
}
|
||||
}
|
||||
}
|
||||
49
syntax.md
49
syntax.md
|
|
@ -394,9 +394,56 @@ ENDSCRIPT
|
|||
- Output from `print()` goes directly to assembler
|
||||
- Can reference compiler variables using `|varname|` syntax
|
||||
- Math module available: `import math`
|
||||
- Maximum 1 million execution steps (prevents infinite loops)
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
**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
|
||||
|
|
|
|||
Loading…
Reference in a new issue