From 161729706713d2c6c866192f39453c53c8af32fb Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Thu, 20 Nov 2025 20:19:45 +0100 Subject: [PATCH] New syntax to PEEK and added tests --- internal/commands/peek.go | 57 ++++- internal/commands/peek_test.go | 416 +++++++++++++++++++++++++++++++++ internal/commands/peekw.go | 57 ++++- 3 files changed, 510 insertions(+), 20 deletions(-) create mode 100644 internal/commands/peek_test.go diff --git a/internal/commands/peek.go b/internal/commands/peek.go index d237f17..dbf91ac 100644 --- a/internal/commands/peek.go +++ b/internal/commands/peek.go @@ -10,7 +10,11 @@ import ( ) // PeekCommand handles PEEK statements -// Syntax: PEEK
[offset] GIVING|-> +// Syntax: +// +// PEEK
[offset] GIVING|-> # old syntax +// = PEEK
[offset] # new syntax +// // Where address is: // - Byte variable (self-modifying code) // - Word variable (self-modifying code or ZP pointer with offset) @@ -38,7 +42,18 @@ func (c *PeekCommand) WillHandle(line preproc.Line) bool { if err != nil || len(params) != 4 { return false } - return strings.ToUpper(params[0]) == "PEEK" + + // Old syntax: PEEK ... (4 params) + if strings.ToUpper(params[0]) == "PEEK" { + return true + } + + // New syntax: = PEEK
+ if params[1] == "=" && strings.ToUpper(params[2]) == "PEEK" { + return true + } + + return false } func (c *PeekCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { @@ -67,14 +82,37 @@ func (c *PeekCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext scope := ctx.CurrentScope() - // Validate separator (param 3) - sep := strings.ToUpper(params[2]) - if sep != "GIVING" && sep != "->" { - return fmt.Errorf("PEEK: parameter #3 must be 'GIVING' or '->', got %q", params[2]) + var addrParam string + var destParam string + + // Determine syntax and parse accordingly + if strings.ToUpper(params[0]) == "PEEK" { + // Old syntax: PEEK
GIVING|-> + + // Validate separator (param 3) + sep := strings.ToUpper(params[2]) + if sep != "GIVING" && sep != "->" { + return fmt.Errorf("PEEK: parameter #3 must be 'GIVING' or '->', got %q", params[2]) + } + + addrParam = params[1] + destParam = params[3] + } else { + // New syntax: = PEEK
+ + if params[1] != "=" { + return fmt.Errorf("PEEK: expected '=' at position 2, got %q", params[1]) + } + + if strings.ToUpper(params[2]) != "PEEK" { + return fmt.Errorf("PEEK: expected 'PEEK' at position 3, got %q", params[2]) + } + + destParam = params[0] + addrParam = params[3] } - // Parse address (param 2) - may have [offset] - addrParam := params[1] + // Parse address - may have [offset] baseAddr, offsetParam := parseOffset(addrParam) constLookup := func(name string) (int64, bool) { @@ -145,8 +183,7 @@ func (c *PeekCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext } } - // Parse destination variable (param 4) - destParam := params[3] + // Parse destination variable destSym := ctx.SymbolTable.Lookup(destParam, scope) if destSym == nil { return fmt.Errorf("PEEK: unknown destination variable %q", destParam) diff --git a/internal/commands/peek_test.go b/internal/commands/peek_test.go new file mode 100644 index 0000000..befeee2 --- /dev/null +++ b/internal/commands/peek_test.go @@ -0,0 +1,416 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestPeekOldSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + }{ + { + name: "peek constant address to byte", + line: "PEEK 1024 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda 1024", + "\tsta result", + }, + }, + { + name: "peek ZP pointer to byte", + line: "PEEK ptr GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddAbsolute("ptr", "", compiler.KindWord, 0x80) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tldy #0", + "\tlda (ptr),y", + "\tsta result", + }, + }, + { + name: "peek ZP pointer with offset to byte", + line: "PEEK ptr[5] GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddAbsolute("ptr", "", compiler.KindWord, 0x80) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tldy #5", + "\tlda (ptr),y", + "\tsta result", + }, + }, + { + name: "peek with -> separator", + line: "PEEK 2048 -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda 2048", + "\tsta result", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + tt.setupVars(ctx.SymbolTable) + + cmd := &PeekCommand{} + line := preproc.Line{ + Text: tt.line, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + if err := cmd.Interpret(line, ctx); err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsm(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestPeekNewSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + }{ + { + name: "new syntax constant address to byte", + line: "result = PEEK 1024", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda 1024", + "\tsta result", + }, + }, + { + name: "new syntax ZP pointer to byte", + line: "result = PEEK ptr", + setupVars: func(st *compiler.SymbolTable) { + st.AddAbsolute("ptr", "", compiler.KindWord, 0x80) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tldy #0", + "\tlda (ptr),y", + "\tsta result", + }, + }, + { + name: "new syntax ZP pointer with offset", + line: "result = PEEK ptr[10]", + setupVars: func(st *compiler.SymbolTable) { + st.AddAbsolute("ptr", "", compiler.KindWord, 0x80) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tldy #10", + "\tlda (ptr),y", + "\tsta result", + }, + }, + { + name: "new syntax ZP pointer with variable offset", + line: "result = PEEK ptr[idx]", + setupVars: func(st *compiler.SymbolTable) { + st.AddAbsolute("ptr", "", compiler.KindWord, 0x80) + st.AddVar("idx", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tldy idx", + "\tlda (ptr),y", + "\tsta result", + }, + }, + { + name: "new syntax to word destination", + line: "result = PEEK 2048", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda 2048", + "\tsta result", + "\tlda #0", + "\tsta result+1", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + tt.setupVars(ctx.SymbolTable) + + cmd := &PeekCommand{} + line := preproc.Line{ + Text: tt.line, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + if err := cmd.Interpret(line, ctx); err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsm(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestPeekWNewSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + }{ + { + name: "new syntax constant address", + line: "result = PEEKW 1024", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda 1024", + "\tsta result", + "\tlda 1025", + "\tsta result+1", + }, + }, + { + name: "new syntax ZP pointer", + line: "result = PEEKW ptr", + setupVars: func(st *compiler.SymbolTable) { + st.AddAbsolute("ptr", "", compiler.KindWord, 0x80) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tldy #0", + "\tlda (ptr),y", + "\tsta result", + "\tiny", + "\tlda (ptr),y", + "\tsta result+1", + }, + }, + { + name: "new syntax ZP pointer with offset", + line: "result = PEEKW ptr[8]", + setupVars: func(st *compiler.SymbolTable) { + st.AddAbsolute("ptr", "", compiler.KindWord, 0x80) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tldy #8", + "\tlda (ptr),y", + "\tsta result", + "\tiny", + "\tlda (ptr),y", + "\tsta result+1", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + tt.setupVars(ctx.SymbolTable) + + cmd := &PeekWCommand{} + line := preproc.Line{ + Text: tt.line, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + if err := cmd.Interpret(line, ctx); err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsm(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestPeekErrors(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantError string + }{ + { + name: "unknown destination", + line: "PEEK 1024 GIVING unknown", + setupVars: func(st *compiler.SymbolTable) { + }, + wantError: "unknown destination variable", + }, + { + name: "constant destination", + line: "PEEK 1024 GIVING CONST", + setupVars: func(st *compiler.SymbolTable) { + st.AddConst("CONST", "", compiler.KindByte, 100) + }, + wantError: "cannot PEEK into constant", + }, + { + name: "new syntax unknown destination", + line: "unknown = PEEK 1024", + setupVars: func(st *compiler.SymbolTable) { + }, + wantError: "unknown destination variable", + }, + { + name: "new syntax constant destination", + line: "CONST = PEEK 1024", + setupVars: func(st *compiler.SymbolTable) { + st.AddConst("CONST", "", compiler.KindByte, 100) + }, + wantError: "cannot PEEK into constant", + }, + { + name: "offset without ZP pointer old syntax", + line: "PEEK addr[5] GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("addr", "", compiler.KindWord, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantError: "offset", + }, + { + name: "offset without ZP pointer new syntax", + line: "result = PEEK addr[5]", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("addr", "", compiler.KindWord, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantError: "offset", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + tt.setupVars(ctx.SymbolTable) + + cmd := &PeekCommand{} + line := preproc.Line{ + Text: tt.line, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + err := cmd.Interpret(line, ctx) + if err == nil { + t.Fatal("Expected error but got none") + } + if !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("Error message mismatch\ngot: %v\nwant substring: %s", err, tt.wantError) + } + }) + } +} + +func TestPeekWErrors(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantError string + }{ + { + name: "byte destination", + line: "PEEKW 1024 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantError: "must be word type", + }, + { + name: "new syntax byte destination", + line: "result = PEEKW 1024", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantError: "must be word type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + tt.setupVars(ctx.SymbolTable) + + cmd := &PeekWCommand{} + line := preproc.Line{ + Text: tt.line, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + err := cmd.Interpret(line, ctx) + if err == nil { + t.Fatal("Expected error but got none") + } + if !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("Error message mismatch\ngot: %v\nwant substring: %s", err, tt.wantError) + } + }) + } +} diff --git a/internal/commands/peekw.go b/internal/commands/peekw.go index 74b0dab..228fdcc 100644 --- a/internal/commands/peekw.go +++ b/internal/commands/peekw.go @@ -10,7 +10,11 @@ import ( ) // PeekWCommand handles PEEKW statements (reads a word/2 bytes) -// Syntax: PEEKW
[offset] GIVING|-> +// Syntax: +// +// PEEKW
[offset] GIVING|-> # old syntax +// = PEEKW
[offset] # new syntax +// // Where address is: // - Byte variable (zero-page indexed addressing) // - Word variable (self-modifying code or ZP pointer with offset) @@ -39,7 +43,18 @@ func (c *PeekWCommand) WillHandle(line preproc.Line) bool { if err != nil || len(params) != 4 { return false } - return strings.ToUpper(params[0]) == "PEEKW" + + // Old syntax: PEEKW ... (4 params) + if strings.ToUpper(params[0]) == "PEEKW" { + return true + } + + // New syntax: = PEEKW
+ if params[1] == "=" && strings.ToUpper(params[2]) == "PEEKW" { + return true + } + + return false } func (c *PeekWCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { @@ -68,14 +83,37 @@ func (c *PeekWCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContex scope := ctx.CurrentScope() - // Validate separator (param 3) - sep := strings.ToUpper(params[2]) - if sep != "GIVING" && sep != "->" { - return fmt.Errorf("PEEKW: parameter #3 must be 'GIVING' or '->', got %q", params[2]) + var addrParam string + var destParam string + + // Determine syntax and parse accordingly + if strings.ToUpper(params[0]) == "PEEKW" { + // Old syntax: PEEKW
GIVING|-> + + // Validate separator (param 3) + sep := strings.ToUpper(params[2]) + if sep != "GIVING" && sep != "->" { + return fmt.Errorf("PEEKW: parameter #3 must be 'GIVING' or '->', got %q", params[2]) + } + + addrParam = params[1] + destParam = params[3] + } else { + // New syntax: = PEEKW
+ + if params[1] != "=" { + return fmt.Errorf("PEEKW: expected '=' at position 2, got %q", params[1]) + } + + if strings.ToUpper(params[2]) != "PEEKW" { + return fmt.Errorf("PEEKW: expected 'PEEKW' at position 3, got %q", params[2]) + } + + destParam = params[0] + addrParam = params[3] } - // Parse address (param 2) - may have [offset] - addrParam := params[1] + // Parse address - may have [offset] baseAddr, offsetParam := parseOffsetW(addrParam) constLookup := func(name string) (int64, bool) { @@ -146,8 +184,7 @@ func (c *PeekWCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContex } } - // Parse destination variable (param 4) - destParam := params[3] + // Parse destination variable destSym := ctx.SymbolTable.Lookup(destParam, scope) if destSym == nil { return fmt.Errorf("PEEKW: unknown destination variable %q", destParam)