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:
parent
b2e9e3dc17
commit
9f24408a0b
6 changed files with 248 additions and 17 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"c65gm/internal/preproc"
|
||||
|
|
@ -211,8 +212,8 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
|
|||
},
|
||||
}
|
||||
|
||||
// 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 + file I/O builtins
|
||||
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
|
||||
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 {
|
||||
|
|
@ -446,8 +464,8 @@ 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 + file I/O builtins + parameter bindings
|
||||
predeclared := starlark.StringDict{
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ func TestExecuteMacro_ErrorDefinitionSite(t *testing.T) {
|
|||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
_, err := ExecuteMacro("div_macro", []string{"10", "0"}, ctx)
|
||||
_, err := ExecuteMacro("div_macro", []string{"10", "0"}, ctx, 0)
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
|
@ -387,7 +387,7 @@ func TestExecuteMacro_ErrorFallbackNoSource(t *testing.T) {
|
|||
Body: []string{"1 // 0"},
|
||||
}
|
||||
|
||||
_, err := ExecuteMacro("m", []string{}, ctx)
|
||||
_, err := ExecuteMacro("m", []string{}, ctx, 0)
|
||||
if err == nil {
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,7 +394,9 @@ 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:**
|
||||
|
|
|
|||
Loading…
Reference in a new issue