Added pragma to control number of steps in scripts and macros. Now that we support file loading some scripts might need more steps to process images or whatnot.

This commit is contained in:
Mattias Hansson 2026-06-12 15:12:34 +02:00
parent b2e9e3dc17
commit 9f24408a0b
6 changed files with 248 additions and 17 deletions

View file

@ -13,6 +13,7 @@ import (
type MacroCommand struct { type MacroCommand struct {
macroName string macroName string
args []string args []string
pragmaSetIndex int
} }
func (c *MacroCommand) WillHandle(line preproc.Line) bool { func (c *MacroCommand) WillHandle(line preproc.Line) bool {
@ -30,11 +31,12 @@ func (c *MacroCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext)
c.macroName = name c.macroName = name
c.args = args c.args = args
c.pragmaSetIndex = line.PragmaSetIndex
return nil return nil
} }
func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
macroOutput, err := compiler.ExecuteMacro(c.macroName, c.args, ctx) macroOutput, err := compiler.ExecuteMacro(c.macroName, c.args, ctx, c.pragmaSetIndex)
if err != nil { if err != nil {
return nil, fmt.Errorf("macro %s: %w", c.macroName, err) return nil, fmt.Errorf("macro %s: %w", c.macroName, err)
} }

View file

@ -169,7 +169,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
return nil, fmt.Errorf("compilation failed") return nil, fmt.Errorf("compilation failed")
} }
macroOutput, err := ExecuteMacro(macroName, args, c.ctx) macroOutput, err := ExecuteMacro(macroName, args, c.ctx, line.PragmaSetIndex)
if err != nil { if err != nil {
c.printErrorWithContext(lines, i, fmt.Errorf("macro %s: %w", macroName, err)) c.printErrorWithContext(lines, i, fmt.Errorf("macro %s: %w", macroName, err))
return nil, fmt.Errorf("compilation failed") return nil, fmt.Errorf("compilation failed")

View file

@ -352,7 +352,7 @@ func TestExecuteMacro_Basic(t *testing.T) {
} }
// Execute macro // Execute macro
output, err := ExecuteMacro("test_macro", []string{"3"}, ctx) output, err := ExecuteMacro("test_macro", []string{"3"}, ctx, 0)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -392,7 +392,7 @@ func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
} }
// Execute macro // Execute macro
output, err := ExecuteMacro("nop_macro", []string{}, ctx) output, err := ExecuteMacro("nop_macro", []string{}, ctx, 0)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -416,7 +416,7 @@ func TestExecuteMacro_StringParameter(t *testing.T) {
} }
// Execute with identifier (should be passed as string) // Execute with identifier (should be passed as string)
output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx) output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx, 0)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -451,7 +451,7 @@ func TestExecuteMacro_LocalVariableExpansion(t *testing.T) {
} }
// Execute macro with "myvar" as argument - should expand |myvar| to testfunc_myvar // Execute macro with "myvar" as argument - should expand |myvar| to testfunc_myvar
output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx) output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx, 0)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }
@ -500,7 +500,7 @@ func TestExecuteMacro_LocalVariableExpansion_MultipleVars(t *testing.T) {
} }
// Execute macro with actual variable names as arguments // Execute macro with actual variable names as arguments
output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx) output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx, 0)
if err != nil { if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err) t.Fatalf("ExecuteMacro failed: %v", err)
} }

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"c65gm/internal/preproc" "c65gm/internal/preproc"
@ -211,8 +212,8 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
}, },
} }
// Set execution limit (prevent infinite loops) // Set execution limit from pragma or default (prevent infinite loops)
thread.SetMaxExecutionSteps(1000000) // 1M steps thread.SetMaxExecutionSteps(readScriptMaxSteps(ctx, scriptLines[0].PragmaSetIndex))
// Build predeclared: math module + library globals + file I/O builtins // Build predeclared: math module + library globals + file I/O builtins
predeclared := starlark.StringDict{ predeclared := starlark.StringDict{
@ -398,8 +399,25 @@ func makeLoadText(projectRoot string) *starlark.Builtin {
}) })
} }
// readScriptMaxSteps reads the _P_SCRIPT_MAX_STEPS pragma from the given pragma set.
// Returns the configured value (must be > 0), or 1000000 as default.
func readScriptMaxSteps(ctx *CompilerContext, pragmaSetIndex int) uint64 {
const defaultSteps uint64 = 1000000
ps := ctx.Pragma.GetPragmaSetByIndex(pragmaSetIndex)
v := ps.GetPragma("_P_SCRIPT_MAX_STEPS")
if v == "" {
return defaultSteps
}
n, err := strconv.ParseUint(v, 10, 64)
if err != nil || n == 0 {
return defaultSteps
}
return n
}
// ExecuteMacro executes a named macro with the given arguments and returns output lines // ExecuteMacro executes a named macro with the given arguments and returns output lines
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 // Look up the macro
macro, ok := ctx.ScriptMacros[macroName] macro, ok := ctx.ScriptMacros[macroName]
if !ok { if !ok {
@ -446,8 +464,8 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
}, },
} }
// Set execution limit // Set execution limit from pragma at call site or default
thread.SetMaxExecutionSteps(1000000) thread.SetMaxExecutionSteps(readScriptMaxSteps(ctx, pragmaSetIndex))
// Build predeclared: math + library globals + file I/O builtins + parameter bindings // Build predeclared: math + library globals + file I/O builtins + parameter bindings
predeclared := starlark.StringDict{ predeclared := starlark.StringDict{

View file

@ -355,7 +355,7 @@ func TestExecuteMacro_ErrorDefinitionSite(t *testing.T) {
r, w, _ := os.Pipe() r, w, _ := os.Pipe()
os.Stderr = w os.Stderr = w
_, err := ExecuteMacro("div_macro", []string{"10", "0"}, ctx) _, err := ExecuteMacro("div_macro", []string{"10", "0"}, ctx, 0)
w.Close() w.Close()
os.Stderr = old os.Stderr = old
@ -387,7 +387,7 @@ func TestExecuteMacro_ErrorFallbackNoSource(t *testing.T) {
Body: []string{"1 // 0"}, Body: []string{"1 // 0"},
} }
_, err := ExecuteMacro("m", []string{}, ctx) _, err := ExecuteMacro("m", []string{}, ctx, 0)
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
@ -842,3 +842,212 @@ func TestExecuteScript_LoadText_LibraryFunction(t *testing.T) {
t.Errorf("output[1] = %q, want \"line1=bar\"", output[1]) t.Errorf("output[1] = %q, want \"line1=bar\"", output[1])
} }
} }
// --- Tests for _P_SCRIPT_MAX_STEPS pragma ---
func TestReadScriptMaxSteps_Default(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// No pragma set — index 0 has empty set
got := readScriptMaxSteps(ctx, 0)
if got != 1000000 {
t.Errorf("readScriptMaxSteps() with no pragma = %d, want 1000000", got)
}
}
func TestReadScriptMaxSteps_CustomValue(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5000000")
ctx := NewCompilerContext(pragma)
got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex())
if got != 5000000 {
t.Errorf("readScriptMaxSteps() = %d, want 5000000", got)
}
}
func TestReadScriptMaxSteps_Zero(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "0")
ctx := NewCompilerContext(pragma)
got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex())
if got != 1000000 {
t.Errorf("readScriptMaxSteps(0) = %d, want 1000000 (default)", got)
}
}
func TestReadScriptMaxSteps_Invalid(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "not-a-number")
ctx := NewCompilerContext(pragma)
got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex())
if got != 1000000 {
t.Errorf("readScriptMaxSteps(invalid) = %d, want 1000000 (default)", got)
}
}
func TestExecuteScript_MaxStepsPragma_LowLimit(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") // very low limit
ctx := NewCompilerContext(pragma)
// A simple loop will exceed 5 steps
scriptLines := []preproc.Line{
{Text: " for i in range(10):", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
{Text: " x = i", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
}
_, err := executeScript(scriptLines, ctx, false)
if err == nil {
t.Fatal("expected step-limit error, got nil")
}
if !strings.Contains(err.Error(), "Starlark error") {
t.Errorf("error should contain 'Starlark error', got: %v", err)
}
}
func TestExecuteScript_MaxStepsPragma_HighLimit(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "500000") // generous limit
ctx := NewCompilerContext(pragma)
scriptLines := []preproc.Line{
{Text: " result = 0", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
{Text: " for i in range(100):", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
{Text: " result = result + i", Filename: "test.c65", LineNo: 3, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
{Text: " print(str(result))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("executeScript failed with generous limit: %v", err)
}
if len(output) < 1 || output[0] != "4950" {
t.Errorf("expected output '4950', got %v", output)
}
}
func TestExecuteMacro_MaxStepsPragma_LowLimit(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") // very low limit
ctx := NewCompilerContext(pragma)
// Register a simple macro
ctx.ScriptMacros["loop_macro"] = &ScriptMacro{
Name: "loop_macro",
Params: []string{},
Body: []string{"for i in range(10):", " x = i"},
SourceFile: "test.c65",
StartLine: 1,
}
_, err := ExecuteMacro("loop_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex())
if err == nil {
t.Fatal("expected step-limit error, got nil")
}
if !strings.Contains(err.Error(), "Starlark error") {
t.Errorf("error should contain 'Starlark error', got: %v", err)
}
}
// Test that two SCRIPT blocks in the same compilation get different step limits
// when a #PRAGMA _P_SCRIPT_MAX_STEPS change appears between them.
func TestExecuteScript_MaxStepsPragma_ChangesBetweenBlocks(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// First block: default limit (index 0, no pragmas) — should succeed
firstLines := []preproc.Line{
{Text: " result = 0", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: 0},
{Text: " for i in range(10):", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: 0},
{Text: " result = result + i", Filename: "test.c65", LineNo: 3, Kind: preproc.Script, PragmaSetIndex: 0},
{Text: " print(str(result))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script, PragmaSetIndex: 0},
}
output, err := executeScript(firstLines, ctx, false)
if err != nil {
t.Fatalf("first script (default limit) should succeed: %v", err)
}
if len(output) < 1 || output[0] != "45" {
t.Errorf("first script output = %v, want ['45']", output)
}
// Simulate #PRAGMA _P_SCRIPT_MAX_STEPS 5 between the blocks
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5")
lowLimitIdx := pragma.GetCurrentPragmaSetIndex()
// Second block: low limit (index from pragma) — should fail
secondLines := []preproc.Line{
{Text: " for i in range(10):", Filename: "test.c65", LineNo: 10, Kind: preproc.Script, PragmaSetIndex: lowLimitIdx},
{Text: " x = i", Filename: "test.c65", LineNo: 11, Kind: preproc.Script, PragmaSetIndex: lowLimitIdx},
}
_, err = executeScript(secondLines, ctx, false)
if err == nil {
t.Fatal("second script (limit=5) should fail with step-limit error, got nil")
}
if !strings.Contains(err.Error(), "Starlark error") {
t.Errorf("second script error should contain 'Starlark error', got: %v", err)
}
}
// Test that two macro invocations in the same compilation get different step limits
// when a #PRAGMA _P_SCRIPT_MAX_STEPS change appears between them.
func TestExecuteMacro_MaxStepsPragma_ChangesBetweenBlocks(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
ctx.ScriptMacros["loop_macro"] = &ScriptMacro{
Name: "loop_macro",
Params: []string{},
Body: []string{"result = 0", "for i in range(10):", " result = result + i", "print(str(result))"},
SourceFile: "test.c65",
StartLine: 1,
}
// First invocation: default limit (first pragma set) — should succeed
output, err := ExecuteMacro("loop_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex())
if err != nil {
t.Fatalf("first macro (default limit) should succeed: %v", err)
}
if len(output) < 1 || output[0] != "45" {
t.Errorf("first macro output = %v, want ['45']", output)
}
// Simulate #PRAGMA _P_SCRIPT_MAX_STEPS 5 between the invocations
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5")
lowLimitIdx := pragma.GetCurrentPragmaSetIndex()
// Second invocation: low limit — should fail
_, err = ExecuteMacro("loop_macro", []string{}, ctx, lowLimitIdx)
if err == nil {
t.Fatal("second macro (limit=5) should fail with step-limit error, got nil")
}
if !strings.Contains(err.Error(), "Starlark error") {
t.Errorf("second macro error should contain 'Starlark error', got: %v", err)
}
}
func TestExecuteMacro_MaxStepsPragma_HighLimit(t *testing.T) {
pragma := preproc.NewPragma()
pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "500000") // generous limit
ctx := NewCompilerContext(pragma)
ctx.ScriptMacros["sum_macro"] = &ScriptMacro{
Name: "sum_macro",
Params: []string{},
Body: []string{"result = 0", "for i in range(100):", " result = result + i", "print(str(result))"},
SourceFile: "test.c65",
StartLine: 1,
}
output, err := ExecuteMacro("sum_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex())
if err != nil {
t.Fatalf("ExecuteMacro failed with generous limit: %v", err)
}
if len(output) < 1 || output[0] != "4950" {
t.Errorf("expected output '4950', got %v", output)
}
}

View file

@ -394,7 +394,9 @@ ENDSCRIPT
- Output from `print()` goes directly to assembler - Output from `print()` goes directly to assembler
- Can reference compiler variables using `|varname|` syntax - Can reference compiler variables using `|varname|` syntax
- Math module available: `import math` - Math module available: `import math`
- Maximum 1 million execution steps (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 - Executed at compile time, not runtime
**File I/O Built-in Functions:** **File I/O Built-in Functions:**