From 0ff627ee0767b4fb2816d6686ff15013f62297fa Mon Sep 17 00:00:00 2001 From: Emanuel Rietveld Date: Fri, 31 Dec 2021 23:16:48 +0000 Subject: [PATCH 1/5] add interp.FilterStack() function to get interpreter stacktrace By placing a handle value on several strategic calls in the runtime we can then later parse a runtime stacktrace, look for the magic values in function parameters, and reconstruct the calling nodes. We can use that to filter out all the yaegi runtime calls and present the user with a stacktrace that includes only interpreted frames. --- interp/interp.go | 258 +++++++++++++++++++++++++++++++++++++++++++++++ interp/run.go | 43 +++++--- interp/scope.go | 27 +++++ 3 files changed, 314 insertions(+), 14 deletions(-) diff --git a/interp/interp.go b/interp/interp.go index 1a5a8bd00..07fbba134 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -19,6 +19,7 @@ import ( "path" "path/filepath" "reflect" + "runtime" "strconv" "strings" "sync" @@ -220,6 +221,7 @@ type Interpreter struct { hooks *hooks // symbol hooks debugger *Debugger + calls map[uintptr]*node // for translating runtime stacktrace, see FilterStack() } const ( @@ -335,6 +337,7 @@ func New(options Options) *Interpreter { pkgNames: map[string]string{}, rdir: map[string]bool{}, hooks: &hooks{}, + calls: map[uintptr]*node{}, } if i.opt.stdin = options.Stdin; i.opt.stdin == nil { @@ -486,6 +489,261 @@ func (interp *Interpreter) resizeFrame() { interp.frame.data = data } +// Add a call with handle that we recognize and can filter from the stacktrace +// Need to make sure this never overlaps with real PCs from runtime.Callers +func (interp *Interpreter) addCall(n *node) uintptr { + handle := reflect.ValueOf(n).Pointer() + interp.calls[handle] = n + return handle +} + +// Return func name as it appears in go stacktraces +func funcName(n *node) string { + if n.scope == nil || n.scope.def == nil { + return "" + } + + // Need to search ancestors for both funcDecl and pkgName + pkgName := n.scope.pkgName + anc := n.scope + ancestors := []*scope{} + funcDeclFound := false + funcDeclIndex := 0 + for anc != nil && anc != anc.anc { + ancestors = append(ancestors, anc) + if anc.def != nil && anc.def.kind == funcDecl && + (anc.anc == nil || anc.anc.def != anc.def) { + funcDeclFound = true + funcDeclIndex = len(ancestors) - 1 + } + if len(anc.pkgName) > 0 { + pkgName = anc.pkgName + } + if len(pkgName) > 0 && funcDeclFound { + break + } + anc = anc.anc + } + + if n.scope.def.typ.recv != nil { + recv := n.scope.def.typ.recv.str + star := "" + if recv[0] == '*' { + star = "*" + recv = recv[1:] + } + recv = strings.TrimPrefix(recv, pkgName+".") + pkgName = fmt.Sprintf("%s.(%s%s)", pkgName, star, recv) + } + + funcName := "" + switch n.scope.def.kind { + case funcDecl: + funcName = n.scope.def.child[1].ident + case funcLit: + counts := []int{} + count := 0 + i := funcDeclIndex + funcName = ancestors[i].def.child[1].ident + for i > 0 { + if ancestors[i].dfs(func(s *scope) dfsSignal { + if s.def != nil && s.def.kind == funcLit && + (s.anc == nil || s.def != s.anc.def) { + count += 1 + } + if s == ancestors[i-1] { + if s.def != nil && s.def.kind == funcLit && + (s.anc == nil || s.def != s.anc.def) { + counts = append(counts, count) + count = 0 + } + i -= 1 + return dfsAbort + } + if s.def != nil && s.def.kind == funcLit { + return dfsSibling + } + return dfsNext + }) != dfsAbort { + // child not found + return "" + } + } + funcName += fmt.Sprintf(".func%d", counts[0]) + for _, count := range counts[1:] { + funcName += fmt.Sprintf(".%d", count) + i += 1 + } + } + return fmt.Sprintf("%s.%s", pkgName, funcName) +} + +// by analogy to runtime.FuncForPC() +type Func struct { + Pos token.Position + Name string + Entry uintptr +} + +func (interp *Interpreter) FuncForCall(handle uintptr) *Func { + n, ok := interp.calls[handle] + if !ok { + return nil + } + pos := n.interp.fset.Position(n.pos) + return &Func{ + pos, + funcName(n), + handle, + } +} + +func (interp *Interpreter) FilterStack(stack []byte) []byte { + newStack, _ := interp.FilterStackAndCallers(stack, []uintptr{}) + return newStack +} + +// Given a runtime stacktrace and callers list, filter out the interpreter runtime +// and replace it with the interpreted calls. Parses runtime stacktrace to figure +// out which interp node by placing a magic value in parameters to runCfg and callBin +func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr) ([]byte, []uintptr) { + newFrames := [][]string{} + newCallers := []uintptr{} + + stackLines := strings.Split(string(stack), "\n") + lastFrame := len(stackLines) + skipFrame := 0 + + const ( + notSyncedYet = -1 + dontSync = -2 + ) + + // index to copy over from callers into newCallers + callersIndex := notSyncedYet // to indicate we haven't synced up with stack yet + if len(callers) == 0 { + callersIndex = dontSync // don't attempt to copy over from callers + } + + // Parse stack in reverse order, because sometimes we want to skip frames + for i := len(stackLines) - 1; i >= 0; i-- { + // Split stack trace into paragraphs (frames) + if len(stackLines[i]) == 0 || stackLines[i][0] == '\t' { + continue + } + + if callersIndex > 0 { + callersIndex-- + } + + if skipFrame > 0 { + lastFrame = i + skipFrame-- + continue + } + + p := stackLines[i:lastFrame] // all lines in single frame + lastFrame = i + + lastSlash := strings.LastIndex(p[0], "/") + funcPath := strings.Split(p[0][lastSlash+1:], ".") + pkgName := p[0][0:lastSlash+1] + funcPath[0] + + if callersIndex >= 0 { + callName := runtime.FuncForPC(callers[callersIndex]).Name() + if callName != strings.Split(p[0], "(")[0] { + // since we're walking stack and callers at the same time they + // should be in sync. If not, we stop messing with callers + for ; callersIndex >= 0; callersIndex-- { + newCallers = append(newCallers, callers[callersIndex]) + } + callersIndex = dontSync + } + } + + // Don't touch any stack frames that aren't in the yaegi runtime + // Functions called on (*Interpreter) may provide information + // on how we entered yaegi, so we pass these through as well + if pkgName != selfPrefix+"/interp" || funcPath[1] == "(*Interpreter)" { + newFrames = append(newFrames, p) + if callersIndex >= 0 { + newCallers = append(newCallers, callers[callersIndex]) + } + continue + } + + var handle uintptr + + // A runCfg call refers to an interpreter level call + // grab callHandle from the first parameter to it + if strings.HasPrefix(funcPath[1], "runCfg(") { + fmt.Sscanf(funcPath[1], "runCfg(%v,", &handle) + // if this is the oldest call to runCfg, sync up with callers array + if callersIndex == notSyncedYet { + for j, call := range callers { + if call == 0 { + break + } + + callName := runtime.FuncForPC(call).Name() + if callName == selfPrefix+"/interp.runCfg" { + callersIndex = j + } + } + for j := len(callers) - 1; j > callersIndex; j-- { + if callers[j] != 0 { + newCallers = append(newCallers, callers[j]) + } + } + } + } + + // callBin is a call to a binPkg + // the callHandle will be on the first or second function literal + if funcPath[1] == "callBin" && + (strings.HasPrefix(funcPath[2], "func1(") || + strings.HasPrefix(funcPath[2], "func2(")) { + fmt.Sscanf(strings.Split(funcPath[2], "(")[1], "%v,", &handle) + // after a binary call, the next two frames will be reflect.Value.Call + skipFrame = 2 + } + + // special case for panic builtin + if funcPath[1] == "_panic" && strings.HasPrefix(funcPath[2], "func1(") { + fmt.Sscanf(strings.Split(funcPath[2], "(")[1], "%v,", &handle) + } + + if handle != 0 { + if callersIndex >= 0 { + newCallers = append(newCallers, handle) + } + f := interp.FuncForCall(handle) + newFrames = append(newFrames, []string{ + f.Name + "()", + fmt.Sprintf("\t%s", f.Pos), + }) + } + } + + // reverse order because we parsed from bottom up, fix that now. + newStack := []string{} + for i := len(newFrames) - 1; i >= 0; i-- { + newStack = append(newStack, newFrames[i]...) + } + unreversedNewCallers := []uintptr{} + for i := len(newCallers) - 1; i >= 0; i-- { + unreversedNewCallers = append(unreversedNewCallers, newCallers[i]) + } + if len(unreversedNewCallers) == 0 { + unreversedNewCallers = callers // just pass the original through + } + + newStackJoined := strings.Join(newStack, "\n") + newStackBytes := make([]byte, len(newStackJoined)-1) + copy(newStackBytes, newStackJoined) + return newStackBytes, unreversedNewCallers +} + // Eval evaluates Go code represented as a string. Eval returns the last result // computed by the interpreter, and a non nil error in case of failure. func (interp *Interpreter) Eval(src string) (res reflect.Value, err error) { diff --git a/interp/run.go b/interp/run.go index 129580782..8659806e9 100644 --- a/interp/run.go +++ b/interp/run.go @@ -117,7 +117,7 @@ func (interp *Interpreter) run(n *node, cf *frame) { for i, t := range n.types { f.data[i] = reflect.New(t).Elem() } - runCfg(n.start, f, n, nil) + runCfg(0, n.start, f, n, nil) } func isExecNode(n *node, exec bltn) bool { @@ -173,9 +173,9 @@ func originalExecNode(n *node, exec bltn) *node { } // Functions set to run during execution of CFG. - // runCfg executes a node AST by walking its CFG and running node builtin at each step. -func runCfg(n *node, f *frame, funcNode, callNode *node) { +// callHandle is just to show up in debug.Stack, see interp.FilterStack(), must be first arg +func runCfg(callHandle uintptr, n *node, f *frame, funcNode, callNode *node) { var exec bltn defer func() { f.mutex.Lock() @@ -900,10 +900,16 @@ func _recover(n *node) { func _panic(n *node) { value := genValue(n.child[1]) + handle := n.interp.addCall(n) - n.exec = func(f *frame) bltn { + // callHandle is to identify this call in debug stacktrace, see interp.FilterStack(). Must be first arg. + panicF := func(callHandle uintptr, f *frame) bltn { panic(value(f)) } + + n.exec = func(f *frame) bltn { + return panicF(handle, f) + } } func genBuiltinDeferWrapper(n *node, in, out []func(*frame) reflect.Value, fn func([]reflect.Value) []reflect.Value) { @@ -1020,7 +1026,8 @@ func genFunctionWrapper(n *node) func(*frame) reflect.Value { } // Interpreter code execution. - runCfg(start, fr, def, n) + callHandle := n.interp.addCall(n) + runCfg(callHandle, start, fr, def, n) result := fr.data[:numRet] for i, r := range result { @@ -1414,12 +1421,14 @@ func call(n *node) { } } + callHandle := n.interp.addCall(n) + // Execute function body if goroutine { - go runCfg(def.child[3].start, nf, def, n) + go runCfg(callHandle, def.child[3].start, nf, def, n) return tnext } - runCfg(def.child[3].start, nf, def, n) + runCfg(callHandle, def.child[3].start, nf, def, n) // Handle branching according to boolean result if fnext != nil && !nf.data[0].Bool() { @@ -1448,6 +1457,7 @@ func getFrame(f *frame, l int) *frame { // Callbin calls a function from a bin import, accessible through reflect. func callBin(n *node) { + handle := n.interp.addCall(n) tnext := getExec(n.tnext) fnext := getExec(n.fnext) child := n.child[1:] @@ -1469,9 +1479,14 @@ func callBin(n *node) { } // Determine if we should use `Call` or `CallSlice` on the function Value. - callFn := func(v reflect.Value, in []reflect.Value) []reflect.Value { return v.Call(in) } + // callHandle is to identify this call in debug stacktrace, see interp.FilterStack(). Must be first arg. + callFn := func(callHandle uintptr, v reflect.Value, in []reflect.Value) []reflect.Value { + return v.Call(in) + } if n.action == aCallSlice { - callFn = func(v reflect.Value, in []reflect.Value) []reflect.Value { return v.CallSlice(in) } + callFn = func(callHandle uintptr, v reflect.Value, in []reflect.Value) []reflect.Value { + return v.CallSlice(in) + } } for i, c := range child { @@ -1566,7 +1581,7 @@ func callBin(n *node) { for i, v := range values { in[i] = v(f) } - go callFn(value(f), in) + go callFn(handle, value(f), in) return tnext } case fnext != nil: @@ -1578,7 +1593,7 @@ func callBin(n *node) { for i, v := range values { in[i] = v(f) } - res := callFn(value(f), in) + res := callFn(handle, value(f), in) b := res[0].Bool() getFrame(f, level).data[index].SetBool(b) if b { @@ -1610,7 +1625,7 @@ func callBin(n *node) { for i, v := range values { in[i] = v(f) } - out := callFn(value(f), in) + out := callFn(handle, value(f), in) for i, v := range rvalues { if v != nil { v(f).Set(out[i]) @@ -1627,7 +1642,7 @@ func callBin(n *node) { for i, v := range values { in[i] = v(f) } - out := callFn(value(f), in) + out := callFn(handle, value(f), in) for i, v := range out { dest := f.data[b+i] if _, ok := dest.Interface().(valueInterface); ok { @@ -1643,7 +1658,7 @@ func callBin(n *node) { for i, v := range values { in[i] = v(f) } - out := callFn(value(f), in) + out := callFn(handle, value(f), in) for i := 0; i < len(out); i++ { r := out[i] if r.Kind() == reflect.Func { diff --git a/interp/scope.go b/interp/scope.go index 6f362ac2b..cc0efe27c 100644 --- a/interp/scope.go +++ b/interp/scope.go @@ -22,6 +22,16 @@ const ( varSym // Variable ) +// Signals from consumer to the depth first search +type dfsSignal uint + +const ( + dfsNext dfsSignal = iota + dfsAbort + dfsSibling + dfsDone +) + var symKinds = [...]string{ undefSym: "undefSym", binSym: "binSym", @@ -241,3 +251,20 @@ func (interp *Interpreter) initScopePkg(pkgID, pkgName string) *scope { interp.mutex.Unlock() return sc } + +// depth first search - iterate over scopes in depth first order +func (s *scope) dfs(f func(*scope) dfsSignal) dfsSignal { + for _, child := range s.child { + signal := f(child) + switch signal { + case dfsAbort: + return dfsAbort + case dfsSibling: + continue + } + if child.dfs(f) == dfsAbort { + return dfsAbort + } + } + return dfsDone +} From 7acf89f3bd4a5027b008ac88c4c98e3f62f5c35e Mon Sep 17 00:00:00 2001 From: Emanuel Rietveld Date: Fri, 31 Dec 2021 23:55:23 +0000 Subject: [PATCH 2/5] interp/FilterStack: filter out non-callExpr this makes the filtered stack look identical to the regular go stacktrace --- interp/interp.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/interp/interp.go b/interp/interp.go index 07fbba134..c48fee9fe 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -717,10 +717,14 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr if callersIndex >= 0 { newCallers = append(newCallers, handle) } - f := interp.FuncForCall(handle) + n, ok := interp.calls[handle] + if !ok || n.kind != callExpr { + continue + } + pos := n.interp.fset.Position(n.pos) newFrames = append(newFrames, []string{ - f.Name + "()", - fmt.Sprintf("\t%s", f.Pos), + funcName(n) + "()", + fmt.Sprintf("\t%s", pos), }) } } From 5262f9a4bbcc653ec1dbe812cf282821a9dd23ce Mon Sep 17 00:00:00 2001 From: Emanuel Rietveld Date: Sat, 1 Jan 2022 07:43:49 +0000 Subject: [PATCH 3/5] interp.Func: implement same interface as runtime.Func this allows calling code to interchange both types --- interp/interp.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/interp/interp.go b/interp/interp.go index c48fee9fe..74ce0663b 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -581,21 +581,33 @@ func funcName(n *node) string { // by analogy to runtime.FuncForPC() type Func struct { Pos token.Position - Name string - Entry uintptr + name string + entry uintptr } -func (interp *Interpreter) FuncForCall(handle uintptr) *Func { +func (f *Func) Entry() uintptr { + return f.entry +} + +func (f *Func) FileLine(pc uintptr) (string, int) { + return f.Pos.Filename, f.Pos.Line +} + +func (f *Func) Name() string { + return f.name +} + +func (interp *Interpreter) FuncForCall(handle uintptr) (*Func, error) { n, ok := interp.calls[handle] if !ok { - return nil + return nil, fmt.Errorf("Call not found") } pos := n.interp.fset.Position(n.pos) return &Func{ pos, funcName(n), handle, - } + }, nil } func (interp *Interpreter) FilterStack(stack []byte) []byte { From a8f4038e7d087318671bdfb2b86592df31a4807f Mon Sep 17 00:00:00 2001 From: Emanuel Rietveld Date: Sun, 2 Jan 2022 19:32:28 +0000 Subject: [PATCH 4/5] interp.FilterStack: don't sync up with callers only on runCfg interpreter may be entered (for example) from genFunctionWrapper --- interp/interp.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/interp/interp.go b/interp/interp.go index 74ce0663b..960a2f272 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -684,30 +684,30 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr continue } + // This is the first call into the interpreter, so try to sync callers + if callersIndex == notSyncedYet { + for j, call := range callers { + if call == 0 { + break + } + callName := runtime.FuncForPC(call).Name() + if callName == strings.Split(p[0], "(")[0] { + callersIndex = j + } + } + for j := len(callers) - 1; j > callersIndex; j-- { + if callers[j] != 0 { + newCallers = append(newCallers, callers[j]) + } + } + } + var handle uintptr // A runCfg call refers to an interpreter level call // grab callHandle from the first parameter to it if strings.HasPrefix(funcPath[1], "runCfg(") { fmt.Sscanf(funcPath[1], "runCfg(%v,", &handle) - // if this is the oldest call to runCfg, sync up with callers array - if callersIndex == notSyncedYet { - for j, call := range callers { - if call == 0 { - break - } - - callName := runtime.FuncForPC(call).Name() - if callName == selfPrefix+"/interp.runCfg" { - callersIndex = j - } - } - for j := len(callers) - 1; j > callersIndex; j-- { - if callers[j] != 0 { - newCallers = append(newCallers, callers[j]) - } - } - } } // callBin is a call to a binPkg From 683535b02c695db42fa66231bf0fb0efe578015d Mon Sep 17 00:00:00 2001 From: Emanuel Rietveld Date: Sun, 2 Jan 2022 20:01:09 +0000 Subject: [PATCH 5/5] 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. --- interp/interp.go | 172 +++++++++++++++++++++++++++++++++++----------- interp/program.go | 7 +- interp/run.go | 20 +++--- 3 files changed, 143 insertions(+), 56 deletions(-) diff --git a/interp/interp.go b/interp/interp.go index 960a2f272..869e38f80 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -20,6 +20,7 @@ import ( "path/filepath" "reflect" "runtime" + "runtime/debug" "strconv" "strings" "sync" @@ -222,6 +223,7 @@ type Interpreter struct { debugger *Debugger calls map[uintptr]*node // for translating runtime stacktrace, see FilterStack() + panics []*Panic // list of panics we have had, see GetOldestPanicForErr() } const ( @@ -250,6 +252,7 @@ var Symbols = Exports{ "Interpreter": reflect.ValueOf((*Interpreter)(nil)), "Options": reflect.ValueOf((*Options)(nil)), "Panic": reflect.ValueOf((*Panic)(nil)), + "IFunc": reflect.ValueOf((*IFunc)(nil)), }, } @@ -263,24 +266,6 @@ type _error struct { func (w _error) Error() string { return w.WError() } -// Panic is an error recovered from a panic call in interpreted code. -type Panic struct { - // Value is the recovered value of a call to panic. - Value interface{} - - // Callers is the call stack obtained from the recover call. - // It may be used as the parameter to runtime.CallersFrames. - Callers []uintptr - - // Stack is the call stack buffer for debug. - Stack []byte -} - -// TODO: Capture interpreter stack frames also and remove -// fmt.Fprintln(n.interp.stderr, oNode.cfgErrorf("panic")) in runCfg. - -func (e Panic) Error() string { return fmt.Sprint(e.Value) } - // Walk traverses AST n in depth first order, call cbin function // at node entry and cbout function at node exit. func (n *node) Walk(in func(n *node) bool, out func(n *node)) { @@ -338,6 +323,7 @@ func New(options Options) *Interpreter { rdir: map[string]bool{}, hooks: &hooks{}, calls: map[uintptr]*node{}, + panics: []*Panic{}, } if i.opt.stdin = options.Stdin; i.opt.stdin == nil { @@ -597,28 +583,46 @@ func (f *Func) Name() string { return f.name } -func (interp *Interpreter) FuncForCall(handle uintptr) (*Func, error) { +type IFunc interface { + Entry() uintptr + FileLine(uintptr) (string, int) + Name() string +} + +// return call if we know it, pass to runtime.FuncForPC otherwise +func (interp *Interpreter) FuncForPC(handle uintptr) IFunc { n, ok := interp.calls[handle] if !ok { - return nil, fmt.Errorf("Call not found") + return runtime.FuncForPC(handle) } pos := n.interp.fset.Position(n.pos) return &Func{ pos, funcName(n), handle, - }, nil + } +} + +func (interp *Interpreter) FilteredStack() []byte { + return interp.FilterStack(debug.Stack()) +} + +func (interp *Interpreter) FilteredCallers() []uintptr { + pc := make([]uintptr, 64) + runtime.Callers(0, pc) + _, fPc := interp.FilterStackAndCallers(debug.Stack(), pc, 2) + return fPc } func (interp *Interpreter) FilterStack(stack []byte) []byte { - newStack, _ := interp.FilterStackAndCallers(stack, []uintptr{}) + newStack, _ := interp.FilterStackAndCallers(stack, []uintptr{}, 2) return newStack } // Given a runtime stacktrace and callers list, filter out the interpreter runtime // and replace it with the interpreted calls. Parses runtime stacktrace to figure // out which interp node by placing a magic value in parameters to runCfg and callBin -func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr) ([]byte, []uintptr) { +func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr, skip int) ([]byte, []uintptr) { newFrames := [][]string{} newCallers := []uintptr{} @@ -638,6 +642,7 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr } // Parse stack in reverse order, because sometimes we want to skip frames + var lastInterpFrame int for i := len(stackLines) - 1; i >= 0; i-- { // Split stack trace into paragraphs (frames) if len(stackLines[i]) == 0 || stackLines[i][0] == '\t' { @@ -664,12 +669,15 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr if callersIndex >= 0 { callName := runtime.FuncForPC(callers[callersIndex]).Name() if callName != strings.Split(p[0], "(")[0] { - // since we're walking stack and callers at the same time they - // should be in sync. If not, we stop messing with callers - for ; callersIndex >= 0; callersIndex-- { - newCallers = append(newCallers, callers[callersIndex]) + // for some reason runtime.gopanic shows up as panic in stacktrace + if callName != "runtime.gopanic" || strings.Split(p[0], "(")[0] != "panic" { + // since we're walking stack and callers at the same time they + // should be in sync. If not, we stop messing with callers + for ; callersIndex >= 0; callersIndex-- { + newCallers = append(newCallers, callers[callersIndex]) + } + callersIndex = dontSync } - callersIndex = dontSync } } @@ -703,6 +711,7 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr } var handle uintptr + originalExecNode := false // A runCfg call refers to an interpreter level call // grab callHandle from the first parameter to it @@ -710,6 +719,12 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr fmt.Sscanf(funcPath[1], "runCfg(%v,", &handle) } + // capture node that panicked + if strings.HasPrefix(funcPath[1], "runCfgPanic(") { + fmt.Sscanf(funcPath[1], "runCfgPanic(%v,", &handle) + originalExecNode = true + } + // callBin is a call to a binPkg // the callHandle will be on the first or second function literal if funcPath[1] == "callBin" && @@ -720,38 +735,53 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr skipFrame = 2 } - // special case for panic builtin - if funcPath[1] == "_panic" && strings.HasPrefix(funcPath[2], "func1(") { - fmt.Sscanf(strings.Split(funcPath[2], "(")[1], "%v,", &handle) - } - if handle != 0 { if callersIndex >= 0 { newCallers = append(newCallers, handle) } n, ok := interp.calls[handle] - if !ok || n.kind != callExpr { + + // Don't print scopes that weren't function calls + // (unless they're the node that caused the panic) + if !ok || (n.kind != callExpr && !originalExecNode) { continue } + pos := n.interp.fset.Position(n.pos) - newFrames = append(newFrames, []string{ + newFrame := []string{ funcName(n) + "()", fmt.Sprintf("\t%s", pos), - }) + } + + // we only find originalExecNode a few frames later + // so place it right after the last interpreted frame + if originalExecNode && len(newFrames) != lastInterpFrame { + newFrames = append( + newFrames[:lastInterpFrame+1], + newFrames[lastInterpFrame:]...) + newFrames[lastInterpFrame] = newFrame + } else { + newFrames = append(newFrames, newFrame) + } + lastInterpFrame = len(newFrames) } } // reverse order because we parsed from bottom up, fix that now. newStack := []string{} - for i := len(newFrames) - 1; i >= 0; i-- { + newStack = append(newStack, newFrames[len(newFrames)-1]...) // skip after goroutine id + for i := len(newFrames) - 2 - skip; i >= 0; i-- { newStack = append(newStack, newFrames[i]...) } unreversedNewCallers := []uintptr{} - for i := len(newCallers) - 1; i >= 0; i-- { - unreversedNewCallers = append(unreversedNewCallers, newCallers[i]) - } - if len(unreversedNewCallers) == 0 { - unreversedNewCallers = callers // just pass the original through + if len(newCallers) == 0 { + if len(callers) >= skip { + unreversedNewCallers = callers[skip:] // just pass the original through + } + } else { + for i := len(newCallers) - 1 - skip; i >= 0; i-- { + unreversedNewCallers = append(unreversedNewCallers, newCallers[i]) + } } newStackJoined := strings.Join(newStack, "\n") @@ -760,6 +790,64 @@ func (interp *Interpreter) FilterStackAndCallers(stack []byte, callers []uintptr return newStackBytes, unreversedNewCallers } +// Panic is an error recovered from a panic call in interpreted code. +type Panic struct { + // Value is the recovered value of a call to panic. + Value interface{} + + // Callers is the call stack obtained from the recover call. + // It may be used as the parameter to runtime.CallersFrames. + Callers []uintptr + + // Stack is the call stack buffer for debug. + Stack []byte + + // Interpreter runtime frames replaced by interpreted code + FilteredCallers []uintptr + FilteredStack []byte +} + +func (e Panic) Error() string { + return fmt.Sprintf("panic: %s\n%s\n", e.Value, e.FilteredStack) +} + +// Store a panic record if this is an error we have not seen. +// Not strictly correct: code might recover from err and never +// call GetOldestPanicForErr(), and we later return the wrong one. +func (interp *Interpreter) Panic(err interface{}) { + if len(interp.panics) > 0 && interp.panics[len(interp.panics)-1].Value == err { + return + } + pc := make([]uintptr, 64) + runtime.Callers(0, pc) + stack := debug.Stack() + fStack, fPc := interp.FilterStackAndCallers(stack, pc, 2) + interp.panics = append(interp.panics, &Panic{ + Value: err, + Callers: pc, + Stack: stack, + FilteredCallers: fPc, + FilteredStack: fStack, + }) +} + +// We want to capture the full stacktrace from where the panic originated. +// Return oldest panic that matches err. Then, clear out the list of panics. +func (interp *Interpreter) GetOldestPanicForErr(err interface{}) *Panic { + if _, ok := err.(*Panic); ok { + return err.(*Panic) + } + r := (*Panic)(nil) + for i := len(interp.panics) - 1; i >= 0; i-- { + if interp.panics[i].Value == err { + r = interp.panics[i] + break + } + } + interp.panics = []*Panic{} + return r +} + // Eval evaluates Go code represented as a string. Eval returns the last result // computed by the interpreter, and a non nil error in case of failure. func (interp *Interpreter) Eval(src string) (res reflect.Value, err error) { diff --git a/interp/program.go b/interp/program.go index 794fa15ac..aa126be65 100644 --- a/interp/program.go +++ b/interp/program.go @@ -5,8 +5,6 @@ import ( "go/ast" "os" "reflect" - "runtime" - "runtime/debug" ) // 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) { defer func() { r := recover() if r != nil { - var pc [64]uintptr // 64 frames should be enough. - n := runtime.Callers(1, pc[:]) - err = Panic{Value: r, Callers: pc[:n], Stack: debug.Stack()} + interp.Panic(r) + err = interp.GetOldestPanicForErr(r) } }() diff --git a/interp/run.go b/interp/run.go index 8659806e9..dc2b26f91 100644 --- a/interp/run.go +++ b/interp/run.go @@ -172,6 +172,12 @@ func originalExecNode(n *node, exec bltn) *node { return originalNode } +// callHandle is just to show up in debug.Stack, see interp.FilterStack(), must be first arg +//go:noinline +func runCfgPanic(callHandle uintptr, o *node, err interface{}) { + o.interp.Panic(err) +} + // Functions set to run during execution of CFG. // runCfg executes a node AST by walking its CFG and running node builtin at each step. // 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) { if oNode == nil { oNode = n } - fmt.Fprintln(n.interp.stderr, oNode.cfgErrorf("panic")) + // capture node that caused panic + handle := oNode.interp.addCall(oNode) + runCfgPanic(handle, oNode, f.recovered) f.mutex.Unlock() panic(f.recovered) } @@ -197,7 +205,7 @@ func runCfg(callHandle uintptr, n *node, f *frame, funcNode, callNode *node) { dbg := n.interp.debugger if dbg == nil { - for exec := n.exec; exec != nil && f.runid() == n.interp.runid(); { + for exec = n.exec; exec != nil && f.runid() == n.interp.runid(); { exec = exec(f) } return @@ -900,15 +908,9 @@ func _recover(n *node) { func _panic(n *node) { value := genValue(n.child[1]) - handle := n.interp.addCall(n) - - // callHandle is to identify this call in debug stacktrace, see interp.FilterStack(). Must be first arg. - panicF := func(callHandle uintptr, f *frame) bltn { - panic(value(f)) - } n.exec = func(f *frame) bltn { - return panicF(handle, f) + panic(value(f)) } }