diff --git a/Dockerfile b/Dockerfile index 0640b5e..48c0a09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/examples/hires/hires.c65 b/examples/hires/hires.c65 index 4c664e4..f3a376e 100644 --- a/examples/hires/hires.c65 +++ b/examples/hires/hires.c65 @@ -8,12 +8,12 @@ FUNC sethires BYTE b b = PEEK $d011 b = b | 32 //enable bitmap mode - POKE $d011 , b + POKE $d011, b b = PEEK $d018 b = b & %11110000 b = b | 8 //enable bitmap mode - POKE $d018 , b + POKE $d018, b FEND @@ -22,7 +22,7 @@ FEND FUNC fillmem({WORD start_addr @ $fa} {WORD end_addr @ $fc} {BYTE value}) WHILE start_addr <= end_addr - POKE start_addr , value + POKE start_addr, value start_addr++ WEND diff --git a/examples/load_binary_demo/load_binary_demo.c65 b/examples/load_binary_demo/load_binary_demo.c65 index 8536f93..6d3def0 100644 --- a/examples/load_binary_demo/load_binary_demo.c65 +++ b/examples/load_binary_demo/load_binary_demo.c65 @@ -42,7 +42,7 @@ LABEL start // Copy 40 bytes from scrdata to screen memory FOR i = 0 TO 39 val = PEEK src - POKE dst WITH val + POKE dst, val src++ dst++ NEXT diff --git a/examples/memlib_demo/memlib_demo.c65 b/examples/memlib_demo/memlib_demo.c65 index 1a1f09f..534707f 100644 --- a/examples/memlib_demo/memlib_demo.c65 +++ b/examples/memlib_demo/memlib_demo.c65 @@ -40,7 +40,7 @@ FUNC wait_key WEND // Reset key buffer - POKE $c6 WITH 0 + POKE $c6, 0 FEND diff --git a/examples/memlib_demo2/memlib_demo2.c65 b/examples/memlib_demo2/memlib_demo2.c65 index f53f332..002c999 100644 --- a/examples/memlib_demo2/memlib_demo2.c65 +++ b/examples/memlib_demo2/memlib_demo2.c65 @@ -45,7 +45,7 @@ FUNC wait_key ENDIF WEND - POKE $c6 WITH 0 + POKE $c6, 0 FEND //----------------------------------------------------------- diff --git a/examples/multdiv_demo/multdiv_demo.c65 b/examples/multdiv_demo/multdiv_demo.c65 index 636d12c..803a60d 100644 --- a/examples/multdiv_demo/multdiv_demo.c65 +++ b/examples/multdiv_demo/multdiv_demo.c65 @@ -36,7 +36,7 @@ FUNC wait_key ENDIF WEND - POKE $c6 WITH 0 + POKE $c6, 0 FEND //----------------------------------------------------------- diff --git a/examples/multicolorbm/cm.sh b/examples/multicolorbm/cm.sh index 5c63744..54bfbb4 100755 --- a/examples/multicolorbm/cm.sh +++ b/examples/multicolorbm/cm.sh @@ -1,5 +1,5 @@ #!/bin/sh # Define filename as variable PROGNAME="multicolorbm" -# Compile and assemble directly -c65gm ${PROGNAME}.c65 \ No newline at end of file +# Compile and assemble directly, keep intermediate .asm file, enable optimizations +c65gm build -i ${PROGNAME}.c65 --keep-asm --opt \ No newline at end of file diff --git a/examples/multicolorbm/multicolorbm.c65 b/examples/multicolorbm/multicolorbm.c65 index 9ec78be..2563620 100644 --- a/examples/multicolorbm/multicolorbm.c65 +++ b/examples/multicolorbm/multicolorbm.c65 @@ -8,16 +8,16 @@ FUNC setmulti BYTE b b = PEEK $d011 b = b | 32 - POKE $d011 , b + POKE $d011, b b = PEEK $d016 b = b | 16 - POKE $d016 , b + POKE $d016, b b = PEEK $d018 b = b & %11110000 b = b | 8 - POKE $d018 , b + POKE $d018, b FEND @@ -25,7 +25,7 @@ FEND FUNC fillmem({WORD start_addr @ $fa} {WORD end_addr @ $fc} {BYTE value}) WHILE start_addr <= end_addr - POKE start_addr , value + POKE start_addr, value start_addr++ WEND @@ -41,7 +41,7 @@ FUNC main fillmem(screen, screen+999, $12) fillmem(colorram, colorram+999, $03) - POKE $d021 , 0 + POKE $d021, 0 WHILE 1 fillmem($2000, $3fff, %00011011) diff --git a/examples/multicolorbm_v2/cm.sh b/examples/multicolorbm_v2/cm.sh new file mode 100755 index 0000000..3b8ea2b --- /dev/null +++ b/examples/multicolorbm_v2/cm.sh @@ -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 diff --git a/examples/multicolorbm_v2/multicolorbm_v2.c65 b/examples/multicolorbm_v2/multicolorbm_v2.c65 new file mode 100644 index 0000000..35a417e --- /dev/null +++ b/examples/multicolorbm_v2/multicolorbm_v2.c65 @@ -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 +#INCLUDE + +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() diff --git a/examples/multicolorbm_v2/start_in_vice.sh b/examples/multicolorbm_v2/start_in_vice.sh new file mode 100755 index 0000000..8c4f61f --- /dev/null +++ b/examples/multicolorbm_v2/start_in_vice.sh @@ -0,0 +1 @@ +x64 -autostartprgmode 1 multicolorbm_v2.prg diff --git a/examples/shift_demo/shift_demo.c65 b/examples/shift_demo/shift_demo.c65 index 02b4b6c..afa1bc8 100644 --- a/examples/shift_demo/shift_demo.c65 +++ b/examples/shift_demo/shift_demo.c65 @@ -43,7 +43,7 @@ FUNC wait_key WEND // Reset key buffer - POKE $c6 WITH 0 + POKE $c6, 0 FEND diff --git a/internal/commands/poke.go b/internal/commands/poke.go index 8a7f55d..69bbc62 100644 --- a/internal/commands/poke.go +++ b/internal/commands/poke.go @@ -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 } diff --git a/internal/commands/poke_test.go b/internal/commands/poke_test.go new file mode 100644 index 0000000..ce694b3 --- /dev/null +++ b/internal/commands/poke_test.go @@ -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")) + } + }) +} diff --git a/internal/commands/pokew.go b/internal/commands/pokew.go index 02dab72..a7d21ed 100644 --- a/internal/commands/pokew.go +++ b/internal/commands/pokew.go @@ -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 } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 3026fcb..14e362f 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -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 { diff --git a/opencode-config/opencode.json b/opencode-config/opencode.json index cc67ed9..1b5dffb 100644 --- a/opencode-config/opencode.json +++ b/opencode-config/opencode.json @@ -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" } } } \ No newline at end of file