@@ -66,6 +66,14 @@ type InterpreterInput struct {
6666 InvokedCommand * ScriptCommand
6767}
6868
69+ // CallFrame represents a function call in the Rad call stack.
70+ // Used for providing stack traces in error messages.
71+ type CallFrame struct {
72+ FunctionName string // Name of the function (or "<anonymous>" for lambdas)
73+ CallSite * Span // Where the function was called from
74+ DefSite * Span // Where the function is defined
75+ }
76+
6977type Interpreter struct {
7078 sd * ScriptData
7179 invokedCommand * ScriptCommand
@@ -76,6 +84,9 @@ type Interpreter struct {
7684 forWhileLoopLevel int
7785 // Used to track current delimiter, currently for correct delimiter escaping handling
7886 delimiterStack * com.Stack [Delimiter ]
87+
88+ // Call stack for Rad function calls (not Go stack)
89+ callStack []CallFrame
7990}
8091
8192func NewInterpreter (input InterpreterInput ) * Interpreter {
@@ -933,6 +944,10 @@ func (i *Interpreter) getOnlyChild(node *ts.Node) *ts.Node {
933944// emitDiagnostic renders a diagnostic and exits with error code 1.
934945// This is the new error reporting method that replaces errorf/errorDetailsf.
935946func (i * Interpreter ) emitDiagnostic (d Diagnostic ) {
947+ // Automatically attach call stack if not already present
948+ if len (d .CallStack ) == 0 && len (i .callStack ) > 0 {
949+ d = d .WithCallStack (i .CallStack ())
950+ }
936951 renderer := NewDiagnosticRenderer (RIo .StdErr )
937952 renderer .Render (d )
938953 RExit .Exit (1 )
@@ -970,6 +985,32 @@ func (i *Interpreter) emitErrorWithSecondary(code rl.Error, primaryNode *ts.Node
970985 i .emitDiagnostic (diag )
971986}
972987
988+ // pushCallFrame pushes a new frame onto the call stack.
989+ func (i * Interpreter ) pushCallFrame (name string , callSite , defSite * Span ) {
990+ i .callStack = append (i .callStack , CallFrame {
991+ FunctionName : name ,
992+ CallSite : callSite ,
993+ DefSite : defSite ,
994+ })
995+ }
996+
997+ // popCallFrame removes the top frame from the call stack.
998+ func (i * Interpreter ) popCallFrame () {
999+ if len (i .callStack ) > 0 {
1000+ i .callStack = i .callStack [:len (i .callStack )- 1 ]
1001+ }
1002+ }
1003+
1004+ // CallStack returns a copy of the current call stack (most recent first).
1005+ func (i * Interpreter ) CallStack () []CallFrame {
1006+ result := make ([]CallFrame , len (i .callStack ))
1007+ // Reverse so most recent is first
1008+ for idx , frame := range i .callStack {
1009+ result [len (i .callStack )- 1 - idx ] = frame
1010+ }
1011+ return result
1012+ }
1013+
9731014// emitUndefinedVariableError emits an error for an undefined variable with
9741015// "did you mean?" suggestions for similar variable names.
9751016func (i * Interpreter ) emitUndefinedVariableError (node * ts.Node , name string ) {
@@ -1296,6 +1337,11 @@ func (i *Interpreter) GetSrc() string {
12961337 return i .sd .Src
12971338}
12981339
1340+ // GetScriptName returns the name/path of the current script.
1341+ func (i * Interpreter ) GetScriptName () string {
1342+ return i .sd .ScriptName
1343+ }
1344+
12991345func (i * Interpreter ) GetSrcForNode (node * ts.Node ) string {
13001346 return i .GetSrc ()[node .StartByte ():node .EndByte ()]
13011347}
@@ -1343,35 +1389,56 @@ func (i *Interpreter) getAssignLeftNodes(node *ts.Node) []ts.Node {
13431389// fallbackNode is used for error reporting when the panic is not a RadPanic.
13441390// msgArgs are optional context values to include in the error message before the panic value.
13451391func (i * Interpreter ) handlePanicRecovery (r interface {}, fallbackNode * ts.Node , msgArgs ... interface {}) {
1346- if r != nil {
1347- radPanic , ok := r .(* RadPanic )
1348- if ok {
1349- err := radPanic .Err ()
1350- msg := err .Msg ().Plain ()
1351- code := rl .ErrGenericRuntime
1352- if ! com .IsBlank (string (err .Code )) {
1353- code = err .Code
1354- }
1355- // Use err.Node if available, otherwise fall back to fallbackNode
1356- node := err .Node
1357- if node == nil {
1358- node = fallbackNode
1359- }
1360- i .emitError (code , node , msg )
1392+ if r == nil {
1393+ return
1394+ }
1395+
1396+ // RadPanic is expected - it's how Rad propagates user-facing errors
1397+ if radPanic , ok := r .(* RadPanic ); ok {
1398+ err := radPanic .Err ()
1399+ msg := err .Msg ().Plain ()
1400+ code := rl .ErrGenericRuntime
1401+ if ! com .IsBlank (string (err .Code )) {
1402+ code = err .Code
13611403 }
1362- if ! IsTest {
1363- // Build error message with panic details
1364- var msgBuilder strings.Builder
1365- msgBuilder .WriteString ("Bug: Panic:" )
1366- for _ , arg := range msgArgs {
1367- msgBuilder .WriteString (fmt .Sprintf (" %v" , arg ))
1368- }
1369- msgBuilder .WriteString (fmt .Sprintf (" %v\n %s" , r , debug .Stack ()))
1370- i .emitError (rl .ErrInternalBug , fallbackNode , msgBuilder .String ())
1404+ // Use err.Node if available, otherwise fall back to fallbackNode
1405+ node := err .Node
1406+ if node == nil {
1407+ node = fallbackNode
13711408 }
1409+ i .emitError (code , node , msg )
1410+ return
1411+ }
1412+
1413+ // Non-RadPanic means an internal bug - this shouldn't happen
1414+ // Skip in tests since test framework may use panics for control flow (e.g., exit simulation)
1415+ if ! IsTest {
1416+ i .emitInternalBug (r , fallbackNode , msgArgs ... )
13721417 }
13731418}
13741419
1420+ // emitInternalBug reports an internal Rad bug to the user.
1421+ // This should only be called for unexpected panics that are NOT RadPanic.
1422+ func (i * Interpreter ) emitInternalBug (panicValue interface {}, fallbackNode * ts.Node , msgArgs ... interface {}) {
1423+ var msgBuilder strings.Builder
1424+ msgBuilder .WriteString ("This is a bug in Rad. Please report it at:\n " )
1425+ msgBuilder .WriteString (" https://github.com/amterp/rad/issues\n \n " )
1426+
1427+ msgBuilder .WriteString ("Panic: " )
1428+ for _ , arg := range msgArgs {
1429+ msgBuilder .WriteString (fmt .Sprintf ("%v " , arg ))
1430+ }
1431+ msgBuilder .WriteString (fmt .Sprintf ("%v\n \n " , panicValue ))
1432+
1433+ if ! IsTest {
1434+ // Include Go stack trace for debugging (skip in tests for deterministic output)
1435+ msgBuilder .WriteString ("Go stack trace:\n " )
1436+ msgBuilder .WriteString (string (debug .Stack ()))
1437+ }
1438+
1439+ i .emitError (rl .ErrInternalBug , fallbackNode , msgBuilder .String ())
1440+ }
1441+
13751442// withCatch wraps body execution with panic catching. If catchNode is nil, just executes body.
13761443// On RadPanic, calls onErr callback to handle the error (assign variables, run catch block, etc.).
13771444// Propagates control flow (return/break/continue/yield) from the catch block.
0 commit comments