Added meaningful error messages for Starlark code
This commit is contained in:
parent
205ae92d0e
commit
fd7c5ecd63
5 changed files with 693 additions and 29 deletions
|
|
@ -42,11 +42,13 @@ func (c *Compiler) Registry() *CommandRegistry {
|
|||
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||
var codeOutput []string
|
||||
var lastKind = preproc.Source
|
||||
var scriptBuffer []string
|
||||
var scriptBuffer []preproc.Line
|
||||
var scriptIsLibrary bool
|
||||
var macroBuffer []string
|
||||
var currentMacroName string
|
||||
var currentMacroParams []string
|
||||
var currentMacroSourceFile string
|
||||
var currentMacroStartLine int
|
||||
var currentAsmTarget *[]string // nil = no active ASM block, or points to target slice
|
||||
|
||||
// Reset deferred ASM storage for this compilation
|
||||
|
|
@ -77,12 +79,16 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
Name: currentMacroName,
|
||||
Params: currentMacroParams,
|
||||
Body: macroBuffer,
|
||||
SourceFile: currentMacroSourceFile,
|
||||
StartLine: currentMacroStartLine,
|
||||
}
|
||||
codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
|
||||
}
|
||||
macroBuffer = nil
|
||||
currentMacroName = ""
|
||||
currentMacroParams = nil
|
||||
currentMacroSourceFile = ""
|
||||
currentMacroStartLine = 0
|
||||
}
|
||||
|
||||
// Close previous Assembler block
|
||||
|
|
@ -199,12 +205,17 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|||
*currentAsmTarget = append(*currentAsmTarget, codePart+commentPart)
|
||||
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
||||
// Collect script lines for execution
|
||||
scriptBuffer = append(scriptBuffer, line.Text)
|
||||
scriptBuffer = append(scriptBuffer, line)
|
||||
} else if line.Kind == preproc.ScriptMacroDef {
|
||||
// Skip the header line (already parsed in transition)
|
||||
if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") {
|
||||
continue
|
||||
}
|
||||
// Capture source provenance from first body line
|
||||
if len(macroBuffer) == 0 {
|
||||
currentMacroSourceFile = line.Filename
|
||||
currentMacroStartLine = line.LineNo
|
||||
}
|
||||
// Collect macro body lines
|
||||
macroBuffer = append(macroBuffer, line.Text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ func TestExecuteScript_BasicPrint(t *testing.T) {
|
|||
" print(' nop')",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ func TestExecuteScript_EmptyOutput(t *testing.T) {
|
|||
"x = 1 + 1",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ func TestExecuteScript_Library_DefinesFunction(t *testing.T) {
|
|||
" print(' nop')",
|
||||
}
|
||||
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
|
|||
" print(' nop')",
|
||||
}
|
||||
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
|
|||
"emit_nops(2)",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("script executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -267,7 +267,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
|||
"def func_a():",
|
||||
" print(' ; from a')",
|
||||
}
|
||||
_, err := executeScript(lib1, ctx, true)
|
||||
_, err := testExecuteScript(lib1, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("lib1 failed: %v", err)
|
||||
}
|
||||
|
|
@ -277,7 +277,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
|||
"def func_b():",
|
||||
" print(' ; from b')",
|
||||
}
|
||||
_, err = executeScript(lib2, ctx, true)
|
||||
_, err = testExecuteScript(lib2, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("lib2 failed: %v", err)
|
||||
}
|
||||
|
|
@ -295,7 +295,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
|||
"func_a()",
|
||||
"func_b()",
|
||||
}
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("script failed: %v", err)
|
||||
}
|
||||
|
|
@ -322,7 +322,7 @@ func TestExecuteScript_RegularScript_DoesNotPersist(t *testing.T) {
|
|||
"local_func()",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("script failed: %v", err)
|
||||
}
|
||||
|
|
@ -377,7 +377,7 @@ func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
|
|||
"def emit_nop():",
|
||||
" print(' nop')",
|
||||
}
|
||||
_, err := executeScript(lib, ctx, true)
|
||||
_, err := testExecuteScript(lib, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library failed: %v", err)
|
||||
}
|
||||
|
|
@ -540,7 +540,7 @@ func TestExecuteScript_LocalVariableExpansion(t *testing.T) {
|
|||
"print(' inc |counter|')",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -569,7 +569,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
|
|||
" print(' inc |global_counter|')",
|
||||
}
|
||||
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library script failed: %v", err)
|
||||
}
|
||||
|
|
@ -579,7 +579,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
|
|||
"inc_global()",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -610,7 +610,7 @@ func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
|
|||
}
|
||||
|
||||
// Library defined at global scope - |local_var| won't find caller's local
|
||||
_, err := executeScript(libraryLines, ctx, true)
|
||||
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library script failed: %v", err)
|
||||
}
|
||||
|
|
@ -625,7 +625,7 @@ func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
|
|||
"use_local()",
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
|
@ -1219,3 +1219,16 @@ func TestAsmAfterVarsWithVariables(t *testing.T) {
|
|||
t.Errorf("expected ASM block in deferred section")
|
||||
}
|
||||
}
|
||||
|
||||
// testExecuteScript is a test helper that wraps executeScript with convenient types
|
||||
func testExecuteScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
|
||||
lines := make([]preproc.Line, len(scriptLines))
|
||||
for i, text := range scriptLines {
|
||||
lines[i] = preproc.Line{
|
||||
Text: text,
|
||||
Filename: "test.c65",
|
||||
LineNo: i + 1,
|
||||
}
|
||||
}
|
||||
return executeScript(lines, ctx, isLibrary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ type ScriptMacro struct {
|
|||
Name string // macro name
|
||||
Params []string // parameter names
|
||||
Body []string // Starlark code lines (the macro body)
|
||||
SourceFile string // source file where macro is defined
|
||||
StartLine int // 1-based line number in source file of first body line
|
||||
}
|
||||
|
||||
// CompilerContext holds all shared resources needed by commands during compilation
|
||||
|
|
|
|||
|
|
@ -2,29 +2,195 @@ package compiler
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"c65gm/internal/preproc"
|
||||
"c65gm/internal/utils"
|
||||
|
||||
"go.starlark.net/lib/math"
|
||||
"go.starlark.net/starlark"
|
||||
)
|
||||
|
||||
// mapStarlarkLine maps a Starlark 1-based line number to an index into scriptLines
|
||||
// (a slice of preproc.Line). Returns -1 if the line cannot be mapped.
|
||||
// For non-library scripts, the Starlark source is:
|
||||
//
|
||||
// Line 1: def _main():
|
||||
// Line 2..N+1: indented script lines
|
||||
// Line N+2: _main()
|
||||
//
|
||||
// For library scripts, lines are used as-is.
|
||||
func mapStarlarkLine(starlarkLine int, numScriptLines int, isLibrary bool) int {
|
||||
var idx int
|
||||
if isLibrary {
|
||||
idx = starlarkLine - 1
|
||||
} else {
|
||||
idx = starlarkLine - 2
|
||||
}
|
||||
if idx < 0 || idx >= numScriptLines {
|
||||
return -1
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// starlarkPosition extracts the source position from a Starlark error.
|
||||
// Returns the 1-based line number (0 if unknown).
|
||||
func starlarkPosition(err error) int {
|
||||
var evalErr *starlark.EvalError
|
||||
if errors.As(err, &evalErr) {
|
||||
if len(evalErr.CallStack) > 0 {
|
||||
top := evalErr.CallStack.At(0)
|
||||
return int(top.Pos.Line)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// starlarkErrorMsg extracts just the message from a Starlark error.
|
||||
func starlarkErrorMsg(err error) string {
|
||||
var evalErr *starlark.EvalError
|
||||
if errors.As(err, &evalErr) {
|
||||
return evalErr.Msg
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// printScriptErrorContext prints a Starlark error with script-block-bounded source context,
|
||||
// synthesizing SCRIPT/ENDSCRIPT boundary lines that the preprocessor discards.
|
||||
// When the original Starlark error has a call stack with frames beyond the script block
|
||||
// (e.g. a library function), those frames are shown as a backtrace below the source context.
|
||||
func printScriptErrorContext(err error, errMsg string, scriptLines []preproc.Line, errorIdx int, blockType string) {
|
||||
if len(scriptLines) == 0 || errorIdx < 0 || errorIdx >= len(scriptLines) {
|
||||
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n\n", errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
line := scriptLines[errorIdx]
|
||||
filename := line.Filename
|
||||
|
||||
const contextLines = 3
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n", errMsg)
|
||||
fmt.Fprintf(os.Stderr, " --> %s:%d\n\n", filename, line.LineNo)
|
||||
|
||||
startIdx := errorIdx - contextLines
|
||||
if startIdx < 0 {
|
||||
startIdx = 0
|
||||
}
|
||||
endIdx := errorIdx + contextLines
|
||||
if endIdx >= len(scriptLines) {
|
||||
endIdx = len(scriptLines) - 1
|
||||
}
|
||||
|
||||
scriptMarkerLineNo := scriptLines[0].LineNo - 1
|
||||
endScriptLineNo := scriptLines[len(scriptLines)-1].LineNo + 1
|
||||
|
||||
maxLineNo := endScriptLineNo
|
||||
if scriptLines[endIdx].LineNo > maxLineNo {
|
||||
maxLineNo = scriptLines[endIdx].LineNo
|
||||
}
|
||||
if scriptMarkerLineNo > maxLineNo {
|
||||
maxLineNo = scriptMarkerLineNo
|
||||
}
|
||||
lineNumWidth := len(fmt.Sprintf("%d", maxLineNo))
|
||||
|
||||
if startIdx == 0 {
|
||||
fmt.Fprintf(os.Stderr, " %*d | %s\n", lineNumWidth, scriptMarkerLineNo, blockType)
|
||||
}
|
||||
|
||||
for i := startIdx; i <= endIdx; i++ {
|
||||
l := scriptLines[i]
|
||||
marker := " "
|
||||
if i == errorIdx {
|
||||
marker = ">> "
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%s%*d | %s\n", marker, lineNumWidth, l.LineNo, l.Text)
|
||||
}
|
||||
|
||||
if endIdx == len(scriptLines)-1 {
|
||||
fmt.Fprintf(os.Stderr, " %*d | END%s\n", lineNumWidth, endScriptLineNo, blockType)
|
||||
}
|
||||
|
||||
// Show call stack if the error has additional frames beyond the script block
|
||||
printStarlarkBacktrace(err, line.LineNo, filename)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
// printStarlarkBacktrace prints a concise call stack from a Starlark error,
|
||||
// filtering out frames from the given scriptBlockLine and <toplevel>/_main wrappers.
|
||||
func printStarlarkBacktrace(err error, scriptBlockLine int, scriptFile string) {
|
||||
var evalErr *starlark.EvalError
|
||||
if !errors.As(err, &evalErr) || len(evalErr.CallStack) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
var frames []string
|
||||
for _, cf := range evalErr.CallStack {
|
||||
name := cf.Name
|
||||
pos := cf.Pos
|
||||
// Skip frames that are already shown in the bounded source context
|
||||
if name == "<toplevel>" || name == "_main" {
|
||||
continue
|
||||
}
|
||||
if int(pos.Line) == scriptBlockLine && pos.Filename() == scriptFile {
|
||||
continue
|
||||
}
|
||||
if pos.Filename() != "" && int(pos.Line) > 0 {
|
||||
frames = append(frames, fmt.Sprintf(" %s: in %s", pos.String(), name))
|
||||
}
|
||||
}
|
||||
if len(frames) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Call stack:\n%s\n", strings.Join(frames, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// printScriptErrorFallback prints what we can from a Starlark error when
|
||||
// we couldn't map it to a specific source line within a script block.
|
||||
func printScriptErrorFallback(err error) {
|
||||
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n", err)
|
||||
var evalErr *starlark.EvalError
|
||||
if errors.As(err, &evalErr) && len(evalErr.CallStack) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Call stack:\n")
|
||||
for _, cf := range evalErr.CallStack {
|
||||
name := cf.Name
|
||||
pos := cf.Pos
|
||||
if pos.Filename() != "" && int(pos.Line) > 0 {
|
||||
fmt.Fprintf(os.Stderr, " %s: in %s\n", pos.String(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
// executeScript runs a Starlark script and returns the output lines.
|
||||
// If isLibrary is true, the script is executed at top level (no _main wrapper)
|
||||
// and resulting globals are persisted to ctx.ScriptLibraryGlobals.
|
||||
func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
|
||||
func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary bool) ([]string, error) {
|
||||
// Extract text from preproc.Lines
|
||||
texts := make([]string, len(scriptLines))
|
||||
for i, l := range scriptLines {
|
||||
texts[i] = l.Text
|
||||
}
|
||||
|
||||
// Join script lines
|
||||
scriptText := strings.Join(scriptLines, "\n")
|
||||
scriptText := strings.Join(texts, "\n")
|
||||
|
||||
// Expand |varname| -> actual variable names
|
||||
scriptText = expandVariables(scriptText, ctx)
|
||||
|
||||
// Determine the source filename for Starlark
|
||||
sourceFile := scriptLines[0].Filename
|
||||
|
||||
var finalScript string
|
||||
var starlarkFilename string
|
||||
if isLibrary {
|
||||
// LIBRARY: execute at top level so defs become globals
|
||||
finalScript = scriptText
|
||||
starlarkFilename = sourceFile
|
||||
} else {
|
||||
// Regular SCRIPT: wrap in function (Starlark requires control flow inside functions)
|
||||
finalScript = "def _main():\n"
|
||||
|
|
@ -32,6 +198,7 @@ func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) (
|
|||
finalScript += " " + line + "\n"
|
||||
}
|
||||
finalScript += "_main()\n"
|
||||
starlarkFilename = sourceFile
|
||||
}
|
||||
|
||||
// Capture print output
|
||||
|
|
@ -55,9 +222,25 @@ func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) (
|
|||
}
|
||||
|
||||
// Execute
|
||||
globals, err := starlark.ExecFile(thread, "script.star", finalScript, predeclared)
|
||||
globals, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Map Starlark error position back to source
|
||||
starLine := starlarkPosition(err)
|
||||
if starLine > 0 {
|
||||
idx := mapStarlarkLine(starLine, len(scriptLines), isLibrary)
|
||||
if idx >= 0 {
|
||||
msg := starlarkErrorMsg(err)
|
||||
blockType := "SCRIPT"
|
||||
if isLibrary {
|
||||
blockType = "SCRIPT LIBRARY"
|
||||
}
|
||||
printScriptErrorContext(err, msg, scriptLines, idx, blockType)
|
||||
return nil, fmt.Errorf("Starlark error: %s", msg)
|
||||
}
|
||||
}
|
||||
// Fallback: print whatever info we can extract
|
||||
printScriptErrorFallback(err)
|
||||
return nil, fmt.Errorf("Starlark error: %w", err)
|
||||
}
|
||||
|
||||
// For LIBRARY: persist new globals (functions, variables defined at top level)
|
||||
|
|
@ -130,6 +313,12 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
|||
}
|
||||
finalScript += "_macro()\n"
|
||||
|
||||
// Use the source file where the macro was defined
|
||||
starlarkFilename := macro.SourceFile
|
||||
if starlarkFilename == "" {
|
||||
starlarkFilename = "macro.star"
|
||||
}
|
||||
|
||||
// Capture print output
|
||||
var output bytes.Buffer
|
||||
thread := &starlark.Thread{
|
||||
|
|
@ -154,9 +343,19 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
|||
}
|
||||
|
||||
// Execute
|
||||
_, err := starlark.ExecFile(thread, "macro.star", finalScript, predeclared)
|
||||
_, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Map error position back to macro definition site
|
||||
starLine := starlarkPosition(err)
|
||||
if starLine > 0 && macro.SourceFile != "" {
|
||||
idx := mapStarlarkLine(starLine, len(macro.Body), false) // macro is always wrapped
|
||||
if idx >= 0 {
|
||||
sourceLine := macro.StartLine + idx
|
||||
msg := starlarkErrorMsg(err)
|
||||
return nil, fmt.Errorf("Starlark error: at %s:%d: %s", macro.SourceFile, sourceLine, msg)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Starlark error: %w", err)
|
||||
}
|
||||
|
||||
// Split output into lines
|
||||
|
|
|
|||
439
internal/compiler/scriptexec_test.go
Normal file
439
internal/compiler/scriptexec_test.go
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"c65gm/internal/preproc"
|
||||
|
||||
"go.starlark.net/lib/math"
|
||||
"go.starlark.net/starlark"
|
||||
)
|
||||
|
||||
// --- Unit tests for mapStarlarkLine ---
|
||||
|
||||
func TestMapStarlarkLine_Regular(t *testing.T) {
|
||||
tests := []struct {
|
||||
starlarkLine int
|
||||
numLines int
|
||||
isLibrary bool
|
||||
want int
|
||||
}{
|
||||
{2, 5, false, 0}, // first script line
|
||||
{3, 5, false, 1}, // second script line
|
||||
{6, 5, false, 4}, // last script line
|
||||
{1, 5, false, -1}, // def _main(): line — out of range
|
||||
{7, 5, false, -1}, // _main() call — out of range
|
||||
{0, 5, false, -1}, // before start
|
||||
{2, 1, false, 0}, // single line script
|
||||
{3, 1, false, -1}, // single line, past it
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := mapStarlarkLine(tt.starlarkLine, tt.numLines, tt.isLibrary)
|
||||
if got != tt.want {
|
||||
t.Errorf("mapStarlarkLine(%d, %d, %v) = %d, want %d",
|
||||
tt.starlarkLine, tt.numLines, tt.isLibrary, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapStarlarkLine_Library(t *testing.T) {
|
||||
tests := []struct {
|
||||
starlarkLine int
|
||||
numLines int
|
||||
want int
|
||||
}{
|
||||
{1, 5, 0}, // first script line
|
||||
{2, 5, 1}, // second script line
|
||||
{5, 5, 4}, // last script line
|
||||
{0, 5, -1}, // before start
|
||||
{6, 5, -1}, // past end
|
||||
{1, 1, 0}, // single line
|
||||
{2, 1, -1}, // single line, past it
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := mapStarlarkLine(tt.starlarkLine, tt.numLines, true)
|
||||
if got != tt.want {
|
||||
t.Errorf("mapStarlarkLine(%d, %d, true) = %d, want %d",
|
||||
tt.starlarkLine, tt.numLines, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for starlarkPosition and starlarkErrorMsg ---
|
||||
|
||||
func TestStarlarkPosition_NoEvalError(t *testing.T) {
|
||||
got := starlarkPosition(fmt.Errorf("plain error"))
|
||||
if got != 0 {
|
||||
t.Errorf("starlarkPosition(plain error) = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarlarkPosition_WithCallStack(t *testing.T) {
|
||||
// Create an EvalError by executing Starlark code with a runtime error
|
||||
thread := &starlark.Thread{}
|
||||
predeclared := starlark.StringDict{"math": math.Module}
|
||||
_, err := starlark.ExecFile(thread, "test.star",
|
||||
`def f():
|
||||
x = 1 // 0
|
||||
f()`,
|
||||
predeclared)
|
||||
if err == nil {
|
||||
t.Fatal("expected Starlark error")
|
||||
}
|
||||
|
||||
pos := starlarkPosition(err)
|
||||
if pos <= 0 {
|
||||
t.Errorf("starlarkPosition() = %d, want > 0 (error: %v)", pos, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarlarkErrorMsg_StripsCallStack(t *testing.T) {
|
||||
thread := &starlark.Thread{}
|
||||
predeclared := starlark.StringDict{"math": math.Module}
|
||||
_, err := starlark.ExecFile(thread, "test.star", `x = 1 // 0`, predeclared)
|
||||
if err == nil {
|
||||
t.Fatal("expected Starlark error")
|
||||
}
|
||||
|
||||
msg := starlarkErrorMsg(err)
|
||||
if !strings.Contains(msg, "division by zero") {
|
||||
t.Errorf("starlarkErrorMsg() = %q, want division by zero", msg)
|
||||
}
|
||||
// Should NOT contain traceback prefixes
|
||||
if strings.Contains(msg, "Traceback") {
|
||||
t.Errorf("starlarkErrorMsg() should not contain Traceback, got %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarlarkErrorMsg_PlainError(t *testing.T) {
|
||||
msg := starlarkErrorMsg(fmt.Errorf("hello"))
|
||||
if msg != "hello" {
|
||||
t.Errorf("starlarkErrorMsg(plain) = %q, want 'hello'", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for printScriptErrorContext ---
|
||||
|
||||
func TestPrintScriptErrorContext_Basic(t *testing.T) {
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: " for i in range(3):", Filename: "test.c65", LineNo: 7, Kind: preproc.Script},
|
||||
{Text: " print(i)", Filename: "test.c65", LineNo: 8, Kind: preproc.Script},
|
||||
{Text: " end", Filename: "test.c65", LineNo: 9, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
// Capture stderr
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
err := fmt.Errorf("test error")
|
||||
printScriptErrorContext(err, "test error", scriptLines, 1, "SCRIPT")
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
// Should contain SCRIPT marker
|
||||
if !strings.Contains(output, "SCRIPT") {
|
||||
t.Errorf("output should contain SCRIPT marker, got:\n%s", output)
|
||||
}
|
||||
// Should contain ENDSCRIPT
|
||||
if !strings.Contains(output, "ENDSCRIPT") {
|
||||
t.Errorf("output should contain ENDSCRIPT, got:\n%s", output)
|
||||
}
|
||||
// Should contain the error line
|
||||
if !strings.Contains(output, "print(i)") {
|
||||
t.Errorf("output should contain the error line, got:\n%s", output)
|
||||
}
|
||||
// Should contain Starlark error header
|
||||
if !strings.Contains(output, "Starlark error:") {
|
||||
t.Errorf("output should contain 'Starlark error:', got:\n%s", output)
|
||||
}
|
||||
// Should contain source marker
|
||||
if !strings.Contains(output, "-->") {
|
||||
t.Errorf("output should contain '-->', got:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintScriptErrorContext_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lines []preproc.Line
|
||||
errorIdx int
|
||||
blockType string
|
||||
wantMarker bool
|
||||
}{
|
||||
{
|
||||
name: "empty lines",
|
||||
lines: []preproc.Line{},
|
||||
errorIdx: 0,
|
||||
blockType: "SCRIPT",
|
||||
wantMarker: false,
|
||||
},
|
||||
{
|
||||
name: "invalid index",
|
||||
lines: []preproc.Line{{Text: "x", Filename: "t", LineNo: 1}},
|
||||
errorIdx: 5,
|
||||
blockType: "SCRIPT",
|
||||
wantMarker: false,
|
||||
},
|
||||
{
|
||||
name: "first line",
|
||||
lines: []preproc.Line{{Text: "x", Filename: "t", LineNo: 1}},
|
||||
errorIdx: 0,
|
||||
blockType: "SCRIPT LIBRARY",
|
||||
wantMarker: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
printScriptErrorContext(fmt.Errorf("err"), "err", tt.lines, tt.errorIdx, tt.blockType)
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
if tt.wantMarker && !strings.Contains(output, tt.blockType) {
|
||||
t.Errorf("output should contain %q, got:\n%s", tt.blockType, output)
|
||||
}
|
||||
if !tt.wantMarker && !strings.Contains(output, "Starlark error") {
|
||||
// Should at minimum print the error
|
||||
t.Errorf("output should contain error even in edge case, got:\n%s", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for executeScript error prefix ---
|
||||
|
||||
func TestExecuteScript_ErrorPrefix(t *testing.T) {
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "def f():\n return 1 // 0\nf()", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
_, err := executeScript(scriptLines, ctx, false)
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "Starlark error:") {
|
||||
t.Errorf("error should contain 'Starlark error:', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LibraryRuntimeError(t *testing.T) {
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
|
||||
// Define a library function that errors
|
||||
libLines := []preproc.Line{
|
||||
{Text: "def bad():\n return 1 // 0", Filename: "test.c65", LineNo: 5, Kind: preproc.ScriptLibrary},
|
||||
}
|
||||
_, err := executeScript(libLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library definition should succeed: %v", err)
|
||||
}
|
||||
|
||||
// Capture stderr
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
// Call the library function — should error
|
||||
callLines := []preproc.Line{
|
||||
{Text: "bad()", Filename: "test.c65", LineNo: 10, Kind: preproc.Script},
|
||||
}
|
||||
_, err = executeScript(callLines, ctx, false)
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
stderr := buf.String()
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error from library call")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "Starlark error:") {
|
||||
t.Errorf("error should contain 'Starlark error:', got: %v", err)
|
||||
}
|
||||
|
||||
// Should show bounded context for the calling SCRIPT block
|
||||
if !strings.Contains(stderr, "SCRIPT") {
|
||||
t.Errorf("stderr should contain SCRIPT marker, got:\n%s", stderr)
|
||||
}
|
||||
if !strings.Contains(stderr, "ENDSCRIPT") {
|
||||
t.Errorf("stderr should contain ENDSCRIPT, got:\n%s", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_DirectRuntimeError(t *testing.T) {
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
|
||||
// Capture stderr
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: " print(1 // 0)", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
|
||||
}
|
||||
_, err := executeScript(scriptLines, ctx, false)
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
stderr := buf.String()
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "Starlark error:") {
|
||||
t.Errorf("error should contain 'Starlark error:', got: %v", err)
|
||||
}
|
||||
|
||||
// Should show the error source line in stderr context
|
||||
if !strings.Contains(stderr, "print(1 // 0)") {
|
||||
t.Errorf("stderr should contain the error line, got:\n%s", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for ExecuteMacro error mapping ---
|
||||
|
||||
func TestExecuteMacro_ErrorDefinitionSite(t *testing.T) {
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
|
||||
// Register a macro with source provenance
|
||||
ctx.ScriptMacros["div_macro"] = &ScriptMacro{
|
||||
Name: "div_macro",
|
||||
Params: []string{"a", "b"},
|
||||
Body: []string{"result = a // b"},
|
||||
SourceFile: "defs.c65",
|
||||
StartLine: 12,
|
||||
}
|
||||
|
||||
// Capture stderr
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
_, err := ExecuteMacro("div_macro", []string{"10", "0"}, ctx)
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
// Error should mention the definition site
|
||||
if !strings.Contains(err.Error(), "defs.c65:12") {
|
||||
t.Errorf("error should reference definition site 'defs.c65:12', got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Starlark error:") {
|
||||
t.Errorf("error should contain 'Starlark error:', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteMacro_ErrorFallbackNoSource(t *testing.T) {
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
|
||||
// Register a macro WITHOUT source provenance (backward compatibility)
|
||||
ctx.ScriptMacros["m"] = &ScriptMacro{
|
||||
Name: "m",
|
||||
Params: []string{},
|
||||
Body: []string{"1 // 0"},
|
||||
}
|
||||
|
||||
_, err := ExecuteMacro("m", []string{}, ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Starlark error:") {
|
||||
t.Errorf("error should contain 'Starlark error:', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for printStarlarkBacktrace ---
|
||||
|
||||
func TestPrintStarlarkBacktrace_NoFrames(t *testing.T) {
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
printStarlarkBacktrace(fmt.Errorf("plain error"), 10, "test.c65")
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
if output != "" {
|
||||
t.Errorf("expected empty output for plain error, got: %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintScriptErrorFallback(t *testing.T) {
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
printScriptErrorFallback(fmt.Errorf("test message"))
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "Starlark error:") {
|
||||
t.Errorf("output should contain 'Starlark error:', got: %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "test message") {
|
||||
t.Errorf("output should contain the error message, got: %q", output)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue