diff --git a/internal/commands/gosub.go b/internal/commands/gosub.go index d1252e9..ee4833c 100644 --- a/internal/commands/gosub.go +++ b/internal/commands/gosub.go @@ -173,7 +173,7 @@ func (c *GosubCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) // Check for immutable code pragma when using variable target if c.isVar { - if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" && c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "0" { return nil, fmt.Errorf("GOSUB: USE_IMMUTABLE_CODE pragma set but variable target requires self-modifying code") } } diff --git a/internal/commands/peek.go b/internal/commands/peek.go index f3fe7eb..6ae3848 100644 --- a/internal/commands/peek.go +++ b/internal/commands/peek.go @@ -196,7 +196,7 @@ func (c *PeekCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) // Check for immutable code pragma when using self-modifying code if c.isAddrVar && !c.isZPPointer { - if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" && c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "0" { return nil, fmt.Errorf("PEEK: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") } } diff --git a/internal/commands/peek_test.go b/internal/commands/peek_test.go index bcca3c8..bfd53ff 100644 --- a/internal/commands/peek_test.go +++ b/internal/commands/peek_test.go @@ -414,3 +414,97 @@ func TestPeekWErrors(t *testing.T) { }) } } + +func TestPeekImmutableCodePragma(t *testing.T) { + // Test that _P_USE_IMMUTABLE_CODE pragma with value "1" causes error for variable address + t.Run("Pragma enabled with 1", func(t *testing.T) { + pragma := preproc.NewPragma() + pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "1") + ctx := compiler.NewCompilerContext(pragma) + + // Add a non-ZP word variable for address + ctx.SymbolTable.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) + + cmd := &PeekCommand{} + line := preproc.Line{ + Text: "PEEK addr GIVING result", + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + // First interpret the command + err := cmd.Interpret(line, ctx) + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + // Then generate code - this is where the pragma check happens + _, err = cmd.Generate(ctx) + if err == nil { + t.Error("Expected error when _P_USE_IMMUTABLE_CODE=1 and using variable address, got nil") + } else if !strings.Contains(err.Error(), "USE_IMMUTABLE_CODE pragma set") { + t.Errorf("Expected error about USE_IMMUTABLE_CODE pragma, got: %v", err) + } + }) + + // Test that _P_USE_IMMUTABLE_CODE pragma with value "0" does NOT cause error + t.Run("Pragma disabled with 0", func(t *testing.T) { + pragma := preproc.NewPragma() + pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "0") + ctx := compiler.NewCompilerContext(pragma) + + // Add a non-ZP word variable for address + ctx.SymbolTable.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) + + cmd := &PeekCommand{} + line := preproc.Line{ + Text: "PEEK addr GIVING result", + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + // First interpret the command + err := cmd.Interpret(line, ctx) + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + // Then generate code - this is where the pragma check happens + _, err = cmd.Generate(ctx) + if err != nil { + t.Errorf("Expected no error when _P_USE_IMMUTABLE_CODE=0, got: %v", err) + } + }) + + // Test that _P_USE_IMMUTABLE_CODE pragma not set does NOT cause error + t.Run("Pragma not set", func(t *testing.T) { + pragma := preproc.NewPragma() + // Don't add the pragma at all + ctx := compiler.NewCompilerContext(pragma) + + // Add a non-ZP word variable for address + ctx.SymbolTable.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) + + cmd := &PeekCommand{} + line := preproc.Line{ + Text: "PEEK addr GIVING result", + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + // First interpret the command + err := cmd.Interpret(line, ctx) + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + // Then generate code - this is where the pragma check happens + _, err = cmd.Generate(ctx) + if err != nil { + t.Errorf("Expected no error when _P_USE_IMMUTABLE_CODE not set, got: %v", err) + } + }) +} diff --git a/internal/commands/peekw.go b/internal/commands/peekw.go index f9f1ff3..35bd69e 100644 --- a/internal/commands/peekw.go +++ b/internal/commands/peekw.go @@ -240,7 +240,7 @@ func (c *PeekWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) // Case 3: Word variable (self-modifying code) if c.isAddrVar && c.addrVarKind == compiler.KindWord { // Check for immutable code pragma - if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" && c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "0" { return nil, fmt.Errorf("PEEKW: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") } diff --git a/internal/commands/poke.go b/internal/commands/poke.go index bef4a59..49e913e 100644 --- a/internal/commands/poke.go +++ b/internal/commands/poke.go @@ -178,7 +178,7 @@ func (c *PokeCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) // Check for immutable code pragma when using self-modifying code if c.isAddrVar && !c.isZPPointer { - if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" && c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "0" { return nil, fmt.Errorf("POKE: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") } } diff --git a/internal/commands/pokew.go b/internal/commands/pokew.go index ea24bce..2767712 100644 --- a/internal/commands/pokew.go +++ b/internal/commands/pokew.go @@ -180,7 +180,7 @@ func (c *PokeWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) // Check for immutable code pragma when using self-modifying code if c.isAddrVar && c.addrVarKind == compiler.KindWord && !c.isZPPointer { - if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" && c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "0" { return nil, fmt.Errorf("POKEW: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") } } diff --git a/internal/compiler/conststr.go b/internal/compiler/conststr.go index 7b01d2c..f7d427c 100644 --- a/internal/compiler/conststr.go +++ b/internal/compiler/conststr.go @@ -29,7 +29,7 @@ func NewConstantStringHandler() *ConstantStringHandler { } func (h *ConstantStringHandler) AddConstStr(label, value string, compress bool, ps preproc.PragmaSet) string { - useCBM := ps.GetPragma("_P_USE_CBM_STRINGS") != "" + useCBM := ps.GetPragma("_P_USE_CBM_STRINGS") != "" && ps.GetPragma("_P_USE_CBM_STRINGS") != "0" if compress { if existingLabel, exists := h.compressMap[value]; exists { diff --git a/internal/compiler/conststr_test.go b/internal/compiler/conststr_test.go index 7cca76e..33991c5 100644 --- a/internal/compiler/conststr_test.go +++ b/internal/compiler/conststr_test.go @@ -109,6 +109,31 @@ func TestGenerateConstStrDecls_CBM(t *testing.T) { } } +func TestGenerateConstStrDecls_CBM_DisabledWithZero(t *testing.T) { + h := NewConstantStringHandler() + ps := mockPragmaSet(map[string]string{"_P_USE_CBM_STRINGS": "0"}) + + h.AddConstStr("str1", "\"hello\"", false, ps) + + result := h.GenerateConstStrDecls() + + expected := []string{ + "str1", + " !raw \"hello\"", + " !8 0", + } + + if len(result) != len(expected) { + t.Fatalf("Expected %d lines, got %d", len(expected), len(result)) + } + + for i, exp := range expected { + if result[i] != exp { + t.Errorf("Line %d: expected %q, got %q", i, exp, result[i]) + } + } +} + func TestGenerateConstStrDecls_MixedPragmas(t *testing.T) { h := NewConstantStringHandler() psNormal := preproc.PragmaSet{} diff --git a/syntax.md b/syntax.md index 3b39d9f..b150034 100644 --- a/syntax.md +++ b/syntax.md @@ -241,11 +241,12 @@ Sets compiler pragmas (options). - Prevents self-modifying code generation - Required for ROM-based code - Errors on PEEK/POKE/GOSUB with variable addresses +- Value: any non-zero value enables **_P_USE_CBM_STRINGS** - Encodes strings in CBM PETSCII format - Default: ASCII encoding -- Value: any non-empty value enables +- Value: any non-zero value enables **_P_IGNORE_UNUSED** - Suppresses "variable declared but never used" warnings