diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index effd7f436..f871db287 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -128,6 +128,30 @@ Example: > step 10`, Func: handleStep, }, + { + Name: "stepinto", + Help: "Stepinto instruction to take in the debugger", + LongHelp: `Usage: stepInto +example: +> stepinto`, + Func: handleStepInto, + }, + { + Name: "stepout", + Help: "Stepout instruction to take in the debugger", + LongHelp: `Usage: stepOut +example: +> stepout`, + Func: handleStepOut, + }, + { + Name: "stepover", + Help: "Stepover instruction to take in the debugger", + LongHelp: `Usage: stepOver +example: +> stepover`, + Func: handleStepOver, + }, { Name: "ops", Help: "Dump opcodes of the current loaded program", @@ -136,7 +160,7 @@ Example: }, } - // VMCLI object for interacting with the VM. +// VMCLI object for interacting with the VM. type VMCLI struct { vm *vm.VM shell *ishell.Shell @@ -297,6 +321,34 @@ func handleStep(c *ishell.Context) { changePrompt(c, v) } +func handleStepInto(c *ishell.Context) { + handleStepType(c, "into") +} + +func handleStepOut(c *ishell.Context) { + handleStepType(c, "out") +} + +func handleStepOver(c *ishell.Context) { + handleStepType(c, "over") +} + +func handleStepType(c *ishell.Context, stepType string) { + if !checkVMIsReady(c) { + return + } + v := getVMFromContext(c) + switch stepType { + case "into": + v.StepInto() + case "out": + v.StepOut() + case "over": + v.StepOver() + } + changePrompt(c, v) +} + func handleOps(c *ishell.Context) { if !checkVMIsReady(c) { return diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 8262f6b0c..2f06bc8b0 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -289,12 +289,84 @@ func (v *VM) Step() { v.execute(ctx, op, param) } +// StepInto behaves the same as “step over” in case if the line does not contain a function it otherwise +// the debugger will enter the called function and continue line-by-line debugging there. +func (v *VM) StepInto() { + ctx := v.Context() + + if ctx == nil { + v.state |= haltState + } + + if v.HasStopped() { + return + } + + if ctx != nil && ctx.prog != nil { + op, param, err := ctx.Next() + if err != nil { + log.Printf("error encountered at instruction %d (%s)", ctx.ip, op) + log.Println(err) + v.state = faultState + } + v.execute(ctx, op, param) + i, op := ctx.CurrInstr() + fmt.Printf("at breakpoint %d (%s)\n", i, op.String()) + } + + cctx := v.Context() + if cctx != nil && cctx.atBreakPoint() { + v.state = breakState + } +} + +// StepOut takes the debugger to the line where the current function was called. +func (v *VM) StepOut() { + if v.state == breakState { + v.state = noneState + } else { + v.state = breakState + } + + expSize := v.istack.len + for v.state.HasFlag(noneState) && v.istack.len >= expSize { + v.StepInto() + } +} + +// StepOver takes the debugger to the line that will step over a given line. +// If the line contains a function the function will be executed and the result returned without debugging each line. +func (v *VM) StepOver() { + if v.HasStopped() { + return + } + + if v.state == breakState { + v.state = noneState + } else { + v.state = breakState + } + + expSize := v.istack.len + for { + v.StepInto() + if !(v.state.HasFlag(noneState) && v.istack.len > expSize) { + break + } + } +} + // HasFailed returns whether VM is in the failed state now. Usually used to // check status after Run. func (v *VM) HasFailed() bool { return v.state.HasFlag(faultState) } +// HasStopped returns whether VM is in Halt or Failed state. +func (v *VM) HasStopped() bool { + return v.state.HasFlag(haltState) || v.state.HasFlag(faultState) +} + // SetCheckedHash sets checked hash for CHECKSIG and CHECKMULTISIG instructions. func (v *VM) SetCheckedHash(h []byte) { v.checkhash = make([]byte, len(h))