diff --git a/gno.land/cmd/gnoland/testdata/panic1.txtar b/gno.land/cmd/gnoland/testdata/panic1.txtar new file mode 100644 index 00000000000..d6ed0d5ebb5 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/panic1.txtar @@ -0,0 +1,26 @@ +# test recursion + +## start a new node +gnoland start + +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/panic -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 + +! gnokey maketx call -pkgpath gno.land/r/demo/panic --func Trigger --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1 + +stderr 'panic\(i\\)' +stderr 'gno.land/r/demo/panic/panic.gno:5' +stderr 'p\\(\)' +stderr 'gno.land/r/demo/panic/panic.gno:9' + +-- panic.gno -- +package main + +func p() { + i := "here" + panic(i) +} + +func Trigger() { + p() +} + diff --git a/gno.land/cmd/gnoland/testdata/panic2.txtar b/gno.land/cmd/gnoland/testdata/panic2.txtar new file mode 100644 index 00000000000..3651a62fb47 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/panic2.txtar @@ -0,0 +1,34 @@ +# test panic recursion +## start a new node +gnoland start + +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/panic -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 + +! gnokey maketx call -pkgpath gno.land/r/demo/panic --func Trigger --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1 + + +stderr 'panic\(\(const \(\"here\" \ string\)\)\)' +stderr 'gno.land/r/demo/panic/panic.gno:5' +stderr 'p\\(i\ \+ \(const \(1 int\)\)\)' +stderr 'gno.land/r/demo/panic/panic.gno:7' +stderr 'p\\(i\ \+ \(const \(1 int\)\)\)' +stderr 'gno.land/r/demo/panic/panic.gno:7' +stderr 'p\\(i\ \+ \(const \(1 int\)\)\)' +stderr 'gno.land/r/demo/panic/panic.gno:7' +stderr 'p\\(\(const \(0 int\)\)\)' +stderr 'gno.land/r/demo/panic/panic.gno:11' + +-- panic.gno -- +package main + +func p(i int) { + if i == 3 { + panic("here") + } + p(i+1) +} + +func Trigger() { + p(0) +} + diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index ef260bd3c42..77e7efbe7c0 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -303,7 +303,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { panic(r) default: err = errors.Wrap(fmt.Errorf("%v", r), "VM call panic: %v\n%s\n", - r, m.String()) + r, m.Stacktrace()) return } } diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 68eb44290e2..d769f3e76ac 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -25,6 +25,8 @@ type Exception struct { // Frame is used to reference the frame a panic occurred in so that recover() knows if the // currently executing deferred function is able to recover from the panic. Frame *Frame + + Stack *Stack } func (e Exception) Sprint(m *Machine) string { @@ -47,6 +49,7 @@ type Machine struct { Package *PackageValue // active package Realm *Realm // active realm Alloc *Allocator // memory allocations + Stack *Stack // stack of expressions Exceptions []Exception NumResults int // number of results returned Cycles int64 // number of "cpu" cycles @@ -164,6 +167,7 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { mm.Store = store mm.Context = context mm.GasMeter = vmGasMeter + mm.Stack = NewStack() mm.Debugger.enabled = opts.Debug mm.Debugger.in = opts.Input mm.Debugger.out = output @@ -678,7 +682,7 @@ func (m *Machine) RunFunc(fn Name) { defer func() { if r := recover(); r != nil { fmt.Printf("Machine.RunFunc(%q) panic: %v\n%s\n", - fn, r, m.String()) + fn, r, m.Stacktrace()) panic(r) } }() @@ -689,7 +693,7 @@ func (m *Machine) RunMain() { defer func() { if r := recover(); r != nil { fmt.Printf("Machine.RunMain() panic: %v\n%s\n", - r, m.String()) + r, m.Stacktrace()) panic(r) } }() @@ -1539,12 +1543,13 @@ func (m *Machine) PopStmt() Stmt { m.Printf("-s %v\n", s) } if bs, ok := s.(*bodyStmt); ok { - return bs.PopActiveStmt() + s = bs.PopActiveStmt() } else { - // general case. m.Stmts = m.Stmts[:numStmts-1] - return s } + + m.Stack.onStmtPopped(s) + return s } func (m *Machine) ForcePopStmt() (s Stmt) { @@ -1740,6 +1745,7 @@ func (m *Machine) PushFrameCall(cx *CallExpr, fv *FuncValue, recv TypedValue) { if rlm != nil && m.Realm != rlm { m.Realm = rlm // enter new realm } + m.Stack.OnFramePushed(fr) } func (m *Machine) PushFrameGoNative(cx *CallExpr, fv *NativeValue) { @@ -1774,6 +1780,7 @@ func (m *Machine) PopFrame() Frame { m.Printf("-F %#v\n", f) } m.Frames = m.Frames[:numFrames-1] + m.Stack.OnFramePopped(f) return *f } @@ -2004,6 +2011,7 @@ func (m *Machine) Panic(ex TypedValue) { Exception{ Value: ex, Frame: m.MustLastCallFrame(1), + Stack: m.Stack.Copy(), }, ) @@ -2148,6 +2156,22 @@ func (m *Machine) String() string { return builder.String() } +func (m *Machine) Stacktrace() string { + var builder strings.Builder + builder.WriteString("Stacktrace:\n") + for i, ex := range m.Exceptions { + builder.WriteString(fmt.Sprintf(" Exception %d: \n ", i)) + + for i := len(ex.Stack.Execs) - 1; i >= 0; i-- { + exec := ex.Stack.Execs[i] + builder.WriteString(fmt.Sprintf(" %v\n", exec.Stmt)) + builder.WriteString(fmt.Sprintf(" %s/%s:%d\n", exec.Fun.PkgPath, exec.Fun.FileName, exec.Stmt.GetLine())) + } + } + + return builder.String() +} + //---------------------------------------- // utility diff --git a/gnovm/pkg/gnolang/stack.go b/gnovm/pkg/gnolang/stack.go new file mode 100644 index 00000000000..0c281ac99b7 --- /dev/null +++ b/gnovm/pkg/gnolang/stack.go @@ -0,0 +1,40 @@ +package gnolang + +type execution struct { + Stmt Stmt + Fun *FuncValue +} +type Stack struct { + Execs []execution +} + +func NewStack() *Stack { + return &Stack{ + Execs: make([]execution, 0), + } +} + +func (s *Stack) onStmtPopped(stmt Stmt) { + if len(s.Execs) > 0 { + s.Execs[len(s.Execs)-1].Stmt = stmt + } +} + +func (s *Stack) OnFramePushed(frame *Frame) { + if frame.Func != nil { + s.Execs = append(s.Execs, execution{Fun: frame.Func}) + } +} + +func (s *Stack) OnFramePopped(frame *Frame) { + if frame.Func != nil { + s.Execs = s.Execs[:len(s.Execs)-1] + } +} + +func (s *Stack) Copy() *Stack { + cpy := NewStack() + cpy.Execs = make([]execution, len(s.Execs)) + copy(cpy.Execs, s.Execs) + return cpy +}