Skip to content

Commit 683535b

Browse files
committed
interp.FilterStack: handle panics and small API improvements
When a panic happens, we want to get the stack trace from the oldest panic, before runCfg unwinds everything. However, at that point we don't know yet whether we will be recovered. As a silly kludge, currently storing the oldest panic in a list on the Interpreter struct which can then be queried once we're ready. The approach taken is not strictly correct: if a panic is recovered, and never queried, and later the same error occurs again and then is not recovered, the wrong call stack will be returned.
1 parent a8f4038 commit 683535b

File tree

3 files changed

+143
-56
lines changed

3 files changed

+143
-56
lines changed

interp/interp.go

Lines changed: 130 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"path/filepath"
2121
"reflect"
2222
"runtime"
23+
"runtime/debug"
2324
"strconv"
2425
"strings"
2526
"sync"
@@ -222,6 +223,7 @@ type Interpreter struct {
222223

223224
debugger *Debugger
224225
calls map[uintptr]*node // for translating runtime stacktrace, see FilterStack()
226+
panics []*Panic // list of panics we have had, see GetOldestPanicForErr()
225227
}
226228

227229
const (
@@ -250,6 +252,7 @@ var Symbols = Exports{
250252
"Interpreter": reflect.ValueOf((*Interpreter)(nil)),
251253
"Options": reflect.ValueOf((*Options)(nil)),
252254
"Panic": reflect.ValueOf((*Panic)(nil)),
255+
"IFunc": reflect.ValueOf((*IFunc)(nil)),
253256
},
254257
}
255258

@@ -263,24 +266,6 @@ type _error struct {
263266

264267
func (w _error) Error() string { return w.WError() }
265268

266-
// Panic is an error recovered from a panic call in interpreted code.
267-
type Panic struct {
268-
// Value is the recovered value of a call to panic.
269-
Value interface{}
270-
271-
// Callers is the call stack obtained from the recover call.
272-
// It may be used as the parameter to runtime.CallersFrames.
273-
Callers []uintptr
274-
275-
// Stack is the call stack buffer for debug.
276-
Stack []byte
277-
}
278-
279-
// TODO: Capture interpreter stack frames also and remove
280-
// fmt.Fprintln(n.interp.stderr, oNode.cfgErrorf("panic")) in runCfg.
281-
282-
func (e Panic) Error() string { return fmt.Sprint(e.Value) }
283-
284269
// Walk traverses AST n in depth first order, call cbin function
285270
// at node entry and cbout function at node exit.
286271
func (n *node) Walk(in func(n *node) bool, out func(n *node)) {
@@ -338,6 +323,7 @@ func New(options Options) *Interpreter {
338323
rdir: map[string]bool{},
339324
hooks: &hooks{},
340325
calls: map[uintptr]*node{},
326+
panics: []*Panic{},
341327
}
342328

343329
if i.opt.stdin = options.Stdin; i.opt.stdin == nil {
@@ -597,28 +583,46 @@ func (f *Func) Name() string {
597583
return f.name
598584
}
599585

600-
func (interp *Interpreter) FuncForCall(handle uintptr) (*Func, error) {
586+
type IFunc interface {
587+
Entry() uintptr
588+
FileLine(uintptr) (string, int)
589+
Name() string
590+
}
591+
592+
// return call if we know it, pass to runtime.FuncForPC otherwise
593+
func (interp *Interpreter) FuncForPC(handle uintptr) IFunc {
601594
n, ok := interp.calls[handle]
602595
if !ok {
603-
return nil, fmt.Errorf("Call not found")
596+
return runtime.FuncForPC(handle)
604597
}
605598
pos := n.interp.fset.Position(n.pos)
606599
return &Func{
607600
pos,
608601
funcName(n),
609602
handle,
610-
}, nil
603+
}
604+
}
605+
606+
func (interp *Interpreter) FilteredStack() []byte {
607+
return interp.FilterStack(debug.Stack())
608+
}
609+
610+
func (interp *Interpreter) FilteredCallers() []uintptr {
611+
pc := make([]uintptr, 64)
612+
runtime.Callers(0, pc)
613+
_, fPc := interp.FilterStackAndCallers(debug.Stack(), pc, 2)
614+
return fPc
611615
}
612616

613617
func (interp *Interpreter) FilterStack(stack []byte) []byte {
614-
newStack, _ := interp.FilterStackAndCallers(stack, []uintptr{})
618+
newStack, _ := interp.FilterStackAndCallers(stack, []uintptr{}, 2)
615619
return newStack
616620
}
617621

618622
// Given a runtime stacktrace and callers list, filter out the interpreter runtime
619623
// and replace it with the interpreted calls. Parses runtime stacktrace to figure
620624
// out which interp node by placing a magic value in parameters to runCfg and callBin
621-
func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr) ([]byte, []uintptr) {
625+
func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr, skip int) ([]byte, []uintptr) {
622626
newFrames := [][]string{}
623627
newCallers := []uintptr{}
624628

@@ -638,6 +642,7 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr
638642
}
639643

640644
// Parse stack in reverse order, because sometimes we want to skip frames
645+
var lastInterpFrame int
641646
for i := len(stackLines) - 1; i >= 0; i-- {
642647
// Split stack trace into paragraphs (frames)
643648
if len(stackLines[i]) == 0 || stackLines[i][0] == '\t' {
@@ -664,12 +669,15 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr
664669
if callersIndex >= 0 {
665670
callName := runtime.FuncForPC(callers[callersIndex]).Name()
666671
if callName != strings.Split(p[0], "(")[0] {
667-
// since we're walking stack and callers at the same time they
668-
// should be in sync. If not, we stop messing with callers
669-
for ; callersIndex >= 0; callersIndex-- {
670-
newCallers = append(newCallers, callers[callersIndex])
672+
// for some reason runtime.gopanic shows up as panic in stacktrace
673+
if callName != "runtime.gopanic" || strings.Split(p[0], "(")[0] != "panic" {
674+
// since we're walking stack and callers at the same time they
675+
// should be in sync. If not, we stop messing with callers
676+
for ; callersIndex >= 0; callersIndex-- {
677+
newCallers = append(newCallers, callers[callersIndex])
678+
}
679+
callersIndex = dontSync
671680
}
672-
callersIndex = dontSync
673681
}
674682
}
675683

@@ -703,13 +711,20 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr
703711
}
704712

705713
var handle uintptr
714+
originalExecNode := false
706715

707716
// A runCfg call refers to an interpreter level call
708717
// grab callHandle from the first parameter to it
709718
if strings.HasPrefix(funcPath[1], "runCfg(") {
710719
fmt.Sscanf(funcPath[1], "runCfg(%v,", &handle)
711720
}
712721

722+
// capture node that panicked
723+
if strings.HasPrefix(funcPath[1], "runCfgPanic(") {
724+
fmt.Sscanf(funcPath[1], "runCfgPanic(%v,", &handle)
725+
originalExecNode = true
726+
}
727+
713728
// callBin is a call to a binPkg
714729
// the callHandle will be on the first or second function literal
715730
if funcPath[1] == "callBin" &&
@@ -720,38 +735,53 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr
720735
skipFrame = 2
721736
}
722737

723-
// special case for panic builtin
724-
if funcPath[1] == "_panic" && strings.HasPrefix(funcPath[2], "func1(") {
725-
fmt.Sscanf(strings.Split(funcPath[2], "(")[1], "%v,", &handle)
726-
}
727-
728738
if handle != 0 {
729739
if callersIndex >= 0 {
730740
newCallers = append(newCallers, handle)
731741
}
732742
n, ok := interp.calls[handle]
733-
if !ok || n.kind != callExpr {
743+
744+
// Don't print scopes that weren't function calls
745+
// (unless they're the node that caused the panic)
746+
if !ok || (n.kind != callExpr && !originalExecNode) {
734747
continue
735748
}
749+
736750
pos := n.interp.fset.Position(n.pos)
737-
newFrames = append(newFrames, []string{
751+
newFrame := []string{
738752
funcName(n) + "()",
739753
fmt.Sprintf("\t%s", pos),
740-
})
754+
}
755+
756+
// we only find originalExecNode a few frames later
757+
// so place it right after the last interpreted frame
758+
if originalExecNode && len(newFrames) != lastInterpFrame {
759+
newFrames = append(
760+
newFrames[:lastInterpFrame+1],
761+
newFrames[lastInterpFrame:]...)
762+
newFrames[lastInterpFrame] = newFrame
763+
} else {
764+
newFrames = append(newFrames, newFrame)
765+
}
766+
lastInterpFrame = len(newFrames)
741767
}
742768
}
743769

744770
// reverse order because we parsed from bottom up, fix that now.
745771
newStack := []string{}
746-
for i := len(newFrames) - 1; i >= 0; i-- {
772+
newStack = append(newStack, newFrames[len(newFrames)-1]...) // skip after goroutine id
773+
for i := len(newFrames) - 2 - skip; i >= 0; i-- {
747774
newStack = append(newStack, newFrames[i]...)
748775
}
749776
unreversedNewCallers := []uintptr{}
750-
for i := len(newCallers) - 1; i >= 0; i-- {
751-
unreversedNewCallers = append(unreversedNewCallers, newCallers[i])
752-
}
753-
if len(unreversedNewCallers) == 0 {
754-
unreversedNewCallers = callers // just pass the original through
777+
if len(newCallers) == 0 {
778+
if len(callers) >= skip {
779+
unreversedNewCallers = callers[skip:] // just pass the original through
780+
}
781+
} else {
782+
for i := len(newCallers) - 1 - skip; i >= 0; i-- {
783+
unreversedNewCallers = append(unreversedNewCallers, newCallers[i])
784+
}
755785
}
756786

757787
newStackJoined := strings.Join(newStack, "\n")
@@ -760,6 +790,64 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr
760790
return newStackBytes, unreversedNewCallers
761791
}
762792

793+
// Panic is an error recovered from a panic call in interpreted code.
794+
type Panic struct {
795+
// Value is the recovered value of a call to panic.
796+
Value interface{}
797+
798+
// Callers is the call stack obtained from the recover call.
799+
// It may be used as the parameter to runtime.CallersFrames.
800+
Callers []uintptr
801+
802+
// Stack is the call stack buffer for debug.
803+
Stack []byte
804+
805+
// Interpreter runtime frames replaced by interpreted code
806+
FilteredCallers []uintptr
807+
FilteredStack []byte
808+
}
809+
810+
func (e Panic) Error() string {
811+
return fmt.Sprintf("panic: %s\n%s\n", e.Value, e.FilteredStack)
812+
}
813+
814+
// Store a panic record if this is an error we have not seen.
815+
// Not strictly correct: code might recover from err and never
816+
// call GetOldestPanicForErr(), and we later return the wrong one.
817+
func (interp *Interpreter) Panic(err interface{}) {
818+
if len(interp.panics) > 0 && interp.panics[len(interp.panics)-1].Value == err {
819+
return
820+
}
821+
pc := make([]uintptr, 64)
822+
runtime.Callers(0, pc)
823+
stack := debug.Stack()
824+
fStack, fPc := interp.FilterStackAndCallers(stack, pc, 2)
825+
interp.panics = append(interp.panics, &Panic{
826+
Value: err,
827+
Callers: pc,
828+
Stack: stack,
829+
FilteredCallers: fPc,
830+
FilteredStack: fStack,
831+
})
832+
}
833+
834+
// We want to capture the full stacktrace from where the panic originated.
835+
// Return oldest panic that matches err. Then, clear out the list of panics.
836+
func (interp *Interpreter) GetOldestPanicForErr(err interface{}) *Panic {
837+
if _, ok := err.(*Panic); ok {
838+
return err.(*Panic)
839+
}
840+
r := (*Panic)(nil)
841+
for i := len(interp.panics) - 1; i >= 0; i-- {
842+
if interp.panics[i].Value == err {
843+
r = interp.panics[i]
844+
break
845+
}
846+
}
847+
interp.panics = []*Panic{}
848+
return r
849+
}
850+
763851
// Eval evaluates Go code represented as a string. Eval returns the last result
764852
// computed by the interpreter, and a non nil error in case of failure.
765853
func (interp *Interpreter) Eval(src string) (res reflect.Value, err error) {

interp/program.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"go/ast"
66
"os"
77
"reflect"
8-
"runtime"
9-
"runtime/debug"
108
)
119

1210
// A Program is Go code that has been parsed and compiled.
@@ -126,9 +124,8 @@ func (interp *Interpreter) Execute(p *Program) (res reflect.Value, err error) {
126124
defer func() {
127125
r := recover()
128126
if r != nil {
129-
var pc [64]uintptr // 64 frames should be enough.
130-
n := runtime.Callers(1, pc[:])
131-
err = Panic{Value: r, Callers: pc[:n], Stack: debug.Stack()}
127+
interp.Panic(r)
128+
err = interp.GetOldestPanicForErr(r)
132129
}
133130
}()
134131

interp/run.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ func originalExecNode(n *node, exec bltn) *node {
172172
return originalNode
173173
}
174174

175+
// callHandle is just to show up in debug.Stack, see interp.FilterStack(), must be first arg
176+
//go:noinline
177+
func runCfgPanic(callHandle uintptr, o *node, err interface{}) {
178+
o.interp.Panic(err)
179+
}
180+
175181
// Functions set to run during execution of CFG.
176182
// runCfg executes a node AST by walking its CFG and running node builtin at each step.
177183
// callHandle is just to show up in debug.Stack, see interp.FilterStack(), must be first arg
@@ -188,7 +194,9 @@ func runCfg(callHandle uintptr, n *node, f *frame, funcNode, callNode *node) {
188194
if oNode == nil {
189195
oNode = n
190196
}
191-
fmt.Fprintln(n.interp.stderr, oNode.cfgErrorf("panic"))
197+
// capture node that caused panic
198+
handle := oNode.interp.addCall(oNode)
199+
runCfgPanic(handle, oNode, f.recovered)
192200
f.mutex.Unlock()
193201
panic(f.recovered)
194202
}
@@ -197,7 +205,7 @@ func runCfg(callHandle uintptr, n *node, f *frame, funcNode, callNode *node) {
197205

198206
dbg := n.interp.debugger
199207
if dbg == nil {
200-
for exec := n.exec; exec != nil && f.runid() == n.interp.runid(); {
208+
for exec = n.exec; exec != nil && f.runid() == n.interp.runid(); {
201209
exec = exec(f)
202210
}
203211
return
@@ -900,15 +908,9 @@ func _recover(n *node) {
900908

901909
func _panic(n *node) {
902910
value := genValue(n.child[1])
903-
handle := n.interp.addCall(n)
904-
905-
// callHandle is to identify this call in debug stacktrace, see interp.FilterStack(). Must be first arg.
906-
panicF := func(callHandle uintptr, f *frame) bltn {
907-
panic(value(f))
908-
}
909911

910912
n.exec = func(f *frame) bltn {
911-
return panicF(handle, f)
913+
panic(value(f))
912914
}
913915
}
914916

0 commit comments

Comments
 (0)