From 21832ab4d6edd3bd4b9905b095f55387a1d38841 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 19 Jan 2024 13:56:06 +0100 Subject: [PATCH 01/37] feat: and an interactive debugger to GnoVM We provide here an embedded interactive debugger to let the user control and inspect its program at symbolic level, with the same features and commands as classical debuggers: gdb, lldb or delve. The debugger is enabled by setting the `-debug` flag in `gno run` command, which loads the target program and immediately shows a debugger prompt on the console: $ gno run -debug /tmp/my-program.gno Welcome to the Gnovm debugger. Type 'help' for list of commands. dbg> Providing `-debug-addr` flag allows to start a remote debugging session, and not interfer with the program stdin and stdout. For example, in a first terminal: $ gno run -debug-addr :4000 /tmp/my-program.gno Waiting for debugger client to connect at :4000 And in a second terminal, using a netcat like nc(1): $ nc localhost 4000 Welcome to the Gnovm debugger. Type 'help' for list of commands. dbg> The debugger works by intercepting each execution step at virtual machine level (each iteration within `Machine.Run` loop) to a callback, which in turns can provide a debugger command REPL or check if the execution can proceed to the next step, etc. This change request is still work-in-progress, as many debugger commands are not ready yet. Nevertheless, the general logic and structure is there. It is possible to `continue`, `stepi`, `detach`, etc and get a general feedback of the user experience and the impact on the code. Efforts are made to make this feature minimally intrusive in the actual VM, and not interfering when the debugger is not used. It is planned shortly after this PR is integrated to add the capacity to attach to an already running program, and to taylor the data format for existing debugging environments such as VScode, etc, as demand arises. --- gnovm/cmd/gno/run.go | 30 ++++-- gnovm/pkg/gnolang/debugger.go | 195 ++++++++++++++++++++++++++++++++++ gnovm/pkg/gnolang/machine.go | 9 ++ 3 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 gnovm/pkg/gnolang/debugger.go diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index 4291d5fe411..cd964905254 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -16,9 +16,11 @@ import ( ) type runCfg struct { - verbose bool - rootDir string - expr string + verbose bool + rootDir string + expr string + debug bool + debugAddr string } func newRunCmd(io commands.IO) *commands.Command { @@ -58,6 +60,20 @@ func (c *runCfg) RegisterFlags(fs *flag.FlagSet) { "main()", "value of expression to evaluate. Defaults to executing function main() with no args", ) + + fs.BoolVar( + &c.debug, + "debug", + false, + "enable interactive debugger using stdin and stdout", + ) + + fs.StringVar( + &c.debugAddr, + "debug-addr", + "", + "enable interactive debugger using tcp address in the form [host]:port", + ) } func execRun(cfg *runCfg, args []string, io commands.IO) error { @@ -96,9 +112,11 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { } m := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: string(files[0].PkgName), - Output: stdout, - Store: testStore, + PkgPath: string(files[0].PkgName), + Output: stdout, + Store: testStore, + Debug: cfg.debug || cfg.debugAddr != "", + DebugAddr: cfg.debugAddr, }) defer m.Release() diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go new file mode 100644 index 00000000000..27b0d09e5cf --- /dev/null +++ b/gnovm/pkg/gnolang/debugger.go @@ -0,0 +1,195 @@ +package gnolang + +import ( + "errors" + "fmt" + "io" + "log" + "net" + "os" + "sort" +) + +// DebugState is the state of the machine debugger. +type DebugState int + +const ( + DebugAtInit DebugState = iota + DebugAtCmd + DebugAtRun + DebugAtExit +) + +// Debugger describes a machine debugger state. +type Debugger struct { + DebugEnabled bool + DebugAddr string + DebugState + lastDebugCmd string + DebugIn io.ReadCloser + DebugOut io.Writer +} + +type debugCommand struct { + debugFunc func(*Machine, string) error // debug command + usage, short, long string // command help texts +} + +var debugCmds map[string]debugCommand +var debugCmdNames []string + +func init() { + log.SetFlags(log.Lshortfile) + + // Register debugger commands. + debugCmds = map[string]debugCommand{ + "continue": {debugContinue, continueUsage, continueShort, ""}, + "detach": {debugDetach, detachUsage, detachShort, ""}, + "exit": {debugExit, exitUsage, exitShort, ""}, + "help": {debugHelp, helpUsage, helpShort, ""}, + "print": {debugPrint, printUsage, printShort, ""}, + "stepi": {debugStepi, stepiUsage, stepiShort, ""}, + } + + // Sort command names for help. + debugCmdNames = make([]string, 0, len(debugCmds)) + for name := range debugCmds { + debugCmdNames = append(debugCmdNames, name) + } + sort.SliceStable(debugCmdNames, func(i, j int) bool { return debugCmdNames[i] < debugCmdNames[j] }) + + // Set command aliases. + debugCmds["c"] = debugCmds["continue"] + debugCmds["h"] = debugCmds["help"] + debugCmds["p"] = debugCmds["print"] + debugCmds["quit"] = debugCmds["exit"] + debugCmds["q"] = debugCmds["exit"] + debugCmds["si"] = debugCmds["stepi"] +} + +// Debug is the debug callback invoked at each VM execution step. +func (m *Machine) Debug() { +loop: + for { + switch m.DebugState { + case DebugAtInit: + initDebugIO(m) + fmt.Fprintln(m.DebugOut, "Welcome to the Gnovm debugger. Type 'help' for list of commands.") + m.DebugState = DebugAtCmd + case DebugAtCmd: + if err := debugCmd(m); err != nil { + fmt.Fprintln(m.DebugOut, "Command failed:", err) + } + case DebugAtRun: + // TODO: here, check matching breakpoint condition and set DebugAtCmd if match. + if m.lastDebugCmd == "stepi" || m.lastDebugCmd == "si" { + m.DebugState = DebugAtCmd + } + break loop + case DebugAtExit: + os.Exit(0) + } + } + fmt.Fprintln(m.DebugOut, "in debug, NumOps:", m.NumOps, ", NumValues:", m.NumValues, len(m.Exprs), len(m.Blocks), len(m.Stmts), m.LastBlock().Source.GetLocation()) +} + +func debugCmd(m *Machine) error { + var cmd, arg string + fmt.Fprint(m.DebugOut, "dbg> ") + if n, err := fmt.Fscanln(m.DebugIn, &cmd, &arg); errors.Is(err, io.EOF) { + return debugDetach(m, arg) + } else if n == 0 { + return nil + } + c, ok := debugCmds[cmd] + if !ok { + return errors.New("command not available: " + cmd) + } + m.lastDebugCmd = cmd + return c.debugFunc(m, arg) +} + +func initDebugIO(m *Machine) { + if m.DebugAddr == "" { + m.DebugIn = os.Stdin + m.DebugOut = os.Stdout + return + } + l, err := net.Listen("tcp", m.DebugAddr) + if err != nil { + panic(err) + } + print("Waiting for debugger client to connect at ", m.DebugAddr) + conn, err := l.Accept() + if err != nil { + panic(err) + } + println(" connected!") + m.DebugIn = conn + m.DebugOut = conn +} + +// --------------------------------------- +const continueUsage = `continue|c` +const continueShort = `Run until breakpoint or program termination.` + +func debugContinue(m *Machine, arg string) error { m.DebugState = DebugAtRun; return nil } + +// --------------------------------------- +const detachUsage = `detach` +const detachShort = `Close debugger and resume program.` + +func debugDetach(m *Machine, arg string) error { + m.DebugEnabled = false + m.DebugState = DebugAtRun + m.DebugIn.Close() + return nil +} + +// --------------------------------------- +const exitUsage = `exit|quit|q` +const exitShort = `Exit the debugger and program.` + +func debugExit(m *Machine, arg string) error { m.DebugState = DebugAtExit; return nil } + +// --------------------------------------- +const helpUsage = `help|h [command]` +const helpShort = `Print the help message.` + +func debugHelp(m *Machine, arg string) error { + c, ok := debugCmds[arg] + if !ok && arg != "" { + return errors.New("command not available") + } + if ok { + t := fmt.Sprintf("%-25s %s", c.usage, c.short) + if c.long != "" { + t += "\n\n" + c.long + } + fmt.Fprintln(m.DebugOut, t) + return nil + } + t := "The followings commands are available:\n\n" + for _, name := range debugCmdNames { + c := debugCmds[name] + t += fmt.Sprintf("%-25s %s\n", c.usage, c.short) + } + t += "\nType help followed by a command for full documentation." + fmt.Fprintln(m.DebugOut, t) + return nil +} + +// --------------------------------------- +const printUsage = `print|p ` +const printShort = `Print a variable or expression.` + +func debugPrint(m *Machine, arg string) error { + println("not implemented yet") + return nil +} + +// --------------------------------------- +const stepiUsage = `stepi|si` +const stepiShort = `Single step a single VM instruction.` + +func debugStepi(m *Machine, arg string) error { m.DebugState = DebugAtRun; return nil } diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 94232e014d2..9d55c77617a 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -36,6 +36,8 @@ type Machine struct { NumResults int // number of results returned Cycles int64 // number of "cpu" cycles + Debugger + // Configuration CheckTypes bool // not yet used ReadOnly bool @@ -66,6 +68,8 @@ type MachineOptions struct { PkgPath string CheckTypes bool // not yet used ReadOnly bool + Debug bool + DebugAddr string // debugger io stream address (stdin/stdout if empty) Output io.Writer Store Store Context interface{} @@ -125,6 +129,8 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { mm.Output = output mm.Store = store mm.Context = context + mm.DebugEnabled = opts.Debug + mm.DebugAddr = opts.DebugAddr if pv != nil { mm.SetActivePackage(pv) @@ -1020,6 +1026,9 @@ const ( func (m *Machine) Run() { for { + if m.DebugEnabled { + m.Debug() + } op := m.PopOp() // TODO: this can be optimized manually, even into tiers. switch op { From 6d550619e08141d8a2eb4aac67256f5c00d57788 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 25 Jan 2024 11:14:07 +0100 Subject: [PATCH 02/37] Determine location in source from VM current state. Improve comments. --- gnovm/pkg/gnolang/debugger.go | 54 ++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 27b0d09e5cf..9571af047e2 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -25,9 +25,11 @@ type Debugger struct { DebugEnabled bool DebugAddr string DebugState + DebugIn io.ReadCloser + DebugOut io.Writer + lastDebugCmd string - DebugIn io.ReadCloser - DebugOut io.Writer + DebugLoc Location } type debugCommand struct { @@ -90,14 +92,18 @@ loop: os.Exit(0) } } - fmt.Fprintln(m.DebugOut, "in debug, NumOps:", m.NumOps, ", NumValues:", m.NumValues, len(m.Exprs), len(m.Blocks), len(m.Stmts), m.LastBlock().Source.GetLocation()) + debugUpdateLoc(m) + fmt.Fprintln(m.DebugOut, "in debug:", m.DebugLoc) } +// debugCmd processes a debugger REPL command. It displays a prompt, then +// reads and parses a command from the debugger input stream, then executes +// the corresponding function or returns an error. func debugCmd(m *Machine) error { var cmd, arg string fmt.Fprint(m.DebugOut, "dbg> ") if n, err := fmt.Fscanln(m.DebugIn, &cmd, &arg); errors.Is(err, io.EOF) { - return debugDetach(m, arg) + return debugDetach(m, arg) // Clean close of debugger, the target program resumes. } else if n == 0 { return nil } @@ -109,6 +115,13 @@ func debugCmd(m *Machine) error { return c.debugFunc(m, arg) } +// initDebugIO initializes the debugger standard input and output streams. +// If no debug address was specified at program start, the debugger will inherit its +// standard input and output from the process, which will be shared with the target program. +// If the debug address is specified, the program will be blocked until +// a client connection is established. The debugger will use this connection and +// not affecting the target program's. +// An error at connection setting will result in program panic. func initDebugIO(m *Machine) { if m.DebugAddr == "" { m.DebugIn = os.Stdin @@ -129,6 +142,39 @@ func initDebugIO(m *Machine) { m.DebugOut = conn } +// debugUpdateLoc computes the source code location of the last VM operation. +// The result is stored in Debugger.DebugLoc. +func debugUpdateLoc(m *Machine) { + loc := m.LastBlock().Source.GetLocation() + + if m.DebugLoc.PkgPath == "" || + loc.PkgPath != "" && loc.PkgPath != m.DebugLoc.PkgPath || + loc.File != "" && loc.File != m.DebugLoc.File { + m.Debugger.DebugLoc = loc + } + + // The location computed from above points to the block start. Examine + // expressions and statements to have the exact line within the block. + + nx := len(m.Exprs) + for i := nx - 1; i >= 0; i-- { + expr := m.Exprs[i] + if l := expr.GetLine(); l > 0 { + m.DebugLoc.Line = l + return + } + } + + if len(m.Stmts) > 0 { + if stmt := m.PeekStmt1(); stmt != nil { + if l := stmt.GetLine(); l > 0 { + m.DebugLoc.Line = l + return + } + } + } +} + // --------------------------------------- const continueUsage = `continue|c` const continueShort = `Run until breakpoint or program termination.` From 103ae9a283705fb287d4cbcd2f8b0361d7950821 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 25 Jan 2024 17:55:14 +0100 Subject: [PATCH 03/37] add list command to print current source code. --- gnovm/pkg/gnolang/debugger.go | 88 +++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 9571af047e2..28115c8460b 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -8,6 +8,7 @@ import ( "net" "os" "sort" + "strings" ) // DebugState is the state of the machine debugger. @@ -29,6 +30,7 @@ type Debugger struct { DebugOut io.Writer lastDebugCmd string + lastDebugArg string DebugLoc Location } @@ -49,6 +51,7 @@ func init() { "detach": {debugDetach, detachUsage, detachShort, ""}, "exit": {debugExit, exitUsage, exitShort, ""}, "help": {debugHelp, helpUsage, helpShort, ""}, + "list": {debugList, listUsage, listShort, ""}, "print": {debugPrint, printUsage, printShort, ""}, "stepi": {debugStepi, stepiUsage, stepiShort, ""}, } @@ -63,6 +66,7 @@ func init() { // Set command aliases. debugCmds["c"] = debugCmds["continue"] debugCmds["h"] = debugCmds["help"] + debugCmds["l"] = debugCmds["list"] debugCmds["p"] = debugCmds["print"] debugCmds["quit"] = debugCmds["exit"] debugCmds["q"] = debugCmds["exit"] @@ -76,6 +80,7 @@ loop: switch m.DebugState { case DebugAtInit: initDebugIO(m) + debugUpdateLoc(m) fmt.Fprintln(m.DebugOut, "Welcome to the Gnovm debugger. Type 'help' for list of commands.") m.DebugState = DebugAtCmd case DebugAtCmd: @@ -93,35 +98,38 @@ loop: } } debugUpdateLoc(m) - fmt.Fprintln(m.DebugOut, "in debug:", m.DebugLoc) } // debugCmd processes a debugger REPL command. It displays a prompt, then // reads and parses a command from the debugger input stream, then executes // the corresponding function or returns an error. +// If the command is empty, the last non-empty command is repeated. func debugCmd(m *Machine) error { var cmd, arg string fmt.Fprint(m.DebugOut, "dbg> ") if n, err := fmt.Fscanln(m.DebugIn, &cmd, &arg); errors.Is(err, io.EOF) { return debugDetach(m, arg) // Clean close of debugger, the target program resumes. } else if n == 0 { - return nil + if m.lastDebugCmd == "" { + return nil + } + cmd, arg = m.lastDebugCmd, m.lastDebugArg } c, ok := debugCmds[cmd] if !ok { return errors.New("command not available: " + cmd) } - m.lastDebugCmd = cmd + m.lastDebugCmd, m.lastDebugArg = cmd, arg return c.debugFunc(m, arg) } // initDebugIO initializes the debugger standard input and output streams. // If no debug address was specified at program start, the debugger will inherit its // standard input and output from the process, which will be shared with the target program. -// If the debug address is specified, the program will be blocked until +// If the debug address was specified, the program will be blocked until // a client connection is established. The debugger will use this connection and // not affecting the target program's. -// An error at connection setting will result in program panic. +// An error during connection setting will result in program panic. func initDebugIO(m *Machine) { if m.DebugAddr == "" { m.DebugIn = os.Stdin @@ -142,7 +150,7 @@ func initDebugIO(m *Machine) { m.DebugOut = conn } -// debugUpdateLoc computes the source code location of the last VM operation. +// debugUpdateLoc computes the source code location for the current VM state. // The result is stored in Debugger.DebugLoc. func debugUpdateLoc(m *Machine) { loc := m.LastBlock().Source.GetLocation() @@ -150,7 +158,7 @@ func debugUpdateLoc(m *Machine) { if m.DebugLoc.PkgPath == "" || loc.PkgPath != "" && loc.PkgPath != m.DebugLoc.PkgPath || loc.File != "" && loc.File != m.DebugLoc.File { - m.Debugger.DebugLoc = loc + m.DebugLoc = loc } // The location computed from above points to the block start. Examine @@ -225,6 +233,66 @@ func debugHelp(m *Machine, arg string) error { return nil } +// --------------------------------------- +const listUsage = `list|l` +const listShort = `Show source code` + +func debugList(m *Machine, arg string) error { + debugLineInfo(m) + lines, offset, err := sourceLines(m.DebugLoc.File, m.DebugLoc.Line) + if err != nil { + return err + } + for i, line := range lines { + cursor := "" + if m.DebugLoc.Line == i+offset { + cursor = "=>" + } + fmt.Fprintf(m.DebugOut, "%2s %4d: %s\n", cursor, i+offset, line) + } + return nil +} + +func debugLineInfo(m *Machine) { + line := string(m.Package.PkgName) + + if len(m.Frames) > 0 { + f := m.Frames[len(m.Frames)-1] + if f.Func != nil { + line += "." + string(f.Func.Name) + "()" + } + } + + fmt.Fprintf(m.DebugOut, "> %s %s:%d\n", line, m.DebugLoc.File, m.DebugLoc.Line) +} + +const listLength = 10 // number of lines to display + +func sourceLines(name string, n int) ([]string, int, error) { + buf, err := os.ReadFile(name) + if err != nil { + return nil, 1, err + } + lines := strings.Split(string(buf), "\n") + start := max(1, n-listLength/2) - 1 + end := min(start+listLength, len(lines)) + return lines[start:end], start + 1, nil +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + // --------------------------------------- const printUsage = `print|p ` const printShort = `Print a variable or expression.` @@ -238,4 +306,8 @@ func debugPrint(m *Machine, arg string) error { const stepiUsage = `stepi|si` const stepiShort = `Single step a single VM instruction.` -func debugStepi(m *Machine, arg string) error { m.DebugState = DebugAtRun; return nil } +func debugStepi(m *Machine, arg string) error { + debugLineInfo(m) + m.DebugState = DebugAtRun + return nil +} From 0cd9c2354b20bba3d802c5d6f5ec533108241b33 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 30 Jan 2024 10:19:44 +0100 Subject: [PATCH 04/37] Implement step command, to single step through program. --- gnovm/pkg/gnolang/debugger.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 28115c8460b..8a23abb5125 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -32,6 +32,7 @@ type Debugger struct { lastDebugCmd string lastDebugArg string DebugLoc Location + PrevDebugLoc Location } type debugCommand struct { @@ -53,6 +54,7 @@ func init() { "help": {debugHelp, helpUsage, helpShort, ""}, "list": {debugList, listUsage, listShort, ""}, "print": {debugPrint, printUsage, printShort, ""}, + "step": {debugStep, stepUsage, stepShort, ""}, "stepi": {debugStepi, stepiUsage, stepiShort, ""}, } @@ -70,6 +72,7 @@ func init() { debugCmds["p"] = debugCmds["print"] debugCmds["quit"] = debugCmds["exit"] debugCmds["q"] = debugCmds["exit"] + debugCmds["s"] = debugCmds["step"] debugCmds["si"] = debugCmds["stepi"] } @@ -89,8 +92,17 @@ loop: } case DebugAtRun: // TODO: here, check matching breakpoint condition and set DebugAtCmd if match. - if m.lastDebugCmd == "stepi" || m.lastDebugCmd == "si" { + switch m.lastDebugCmd { + case "si", "stepi": m.DebugState = DebugAtCmd + break + case "s", "step": + if m.DebugLoc != m.PrevDebugLoc { + m.DebugState = DebugAtCmd + m.PrevDebugLoc = m.DebugLoc + debugList(m, "") + break + } } break loop case DebugAtExit: @@ -302,6 +314,15 @@ func debugPrint(m *Machine, arg string) error { return nil } +// --------------------------------------- +const stepUsage = `step|s` +const stepShort = `Single step through program.` + +func debugStep(m *Machine, arg string) error { + m.DebugState = DebugAtRun + return nil +} + // --------------------------------------- const stepiUsage = `stepi|si` const stepiShort = `Single step a single VM instruction.` From 80b17c419efd084f8c0080344448a4e06f896a54 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 30 Jan 2024 15:30:21 +0100 Subject: [PATCH 05/37] Implement breakpoint setting, listing and clearing. --- gnovm/pkg/gnolang/debugger.go | 153 +++++++++++++++++++++++++++------- 1 file changed, 121 insertions(+), 32 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 8a23abb5125..763327bee8d 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -7,7 +7,10 @@ import ( "log" "net" "os" + "path" + "path/filepath" "sort" + "strconv" "strings" ) @@ -33,6 +36,7 @@ type Debugger struct { lastDebugArg string DebugLoc Location PrevDebugLoc Location + breakpoints []Location } type debugCommand struct { @@ -48,14 +52,17 @@ func init() { // Register debugger commands. debugCmds = map[string]debugCommand{ - "continue": {debugContinue, continueUsage, continueShort, ""}, - "detach": {debugDetach, detachUsage, detachShort, ""}, - "exit": {debugExit, exitUsage, exitShort, ""}, - "help": {debugHelp, helpUsage, helpShort, ""}, - "list": {debugList, listUsage, listShort, ""}, - "print": {debugPrint, printUsage, printShort, ""}, - "step": {debugStep, stepUsage, stepShort, ""}, - "stepi": {debugStepi, stepiUsage, stepiShort, ""}, + "break": {debugBreak, breakUsage, breakShort, breakLong}, + "breakpoints": {debugBreakpoints, breakpointsUsage, breakpointsShort, ""}, + "clear": {debugClear, clearUsage, clearShort, ""}, + "continue": {debugContinue, continueUsage, continueShort, ""}, + "detach": {debugDetach, detachUsage, detachShort, ""}, + "exit": {debugExit, exitUsage, exitShort, ""}, + "help": {debugHelp, helpUsage, helpShort, ""}, + "list": {debugList, listUsage, listShort, ""}, + "print": {debugPrint, printUsage, printShort, ""}, + "step": {debugContinue, stepUsage, stepShort, ""}, + "stepi": {debugContinue, stepiUsage, stepiShort, ""}, } // Sort command names for help. @@ -66,6 +73,8 @@ func init() { sort.SliceStable(debugCmdNames, func(i, j int) bool { return debugCmdNames[i] < debugCmdNames[j] }) // Set command aliases. + debugCmds["b"] = debugCmds["break"] + debugCmds["bp"] = debugCmds["breakpoints"] debugCmds["c"] = debugCmds["continue"] debugCmds["h"] = debugCmds["help"] debugCmds["l"] = debugCmds["list"] @@ -91,17 +100,25 @@ loop: fmt.Fprintln(m.DebugOut, "Command failed:", err) } case DebugAtRun: - // TODO: here, check matching breakpoint condition and set DebugAtCmd if match. switch m.lastDebugCmd { case "si", "stepi": m.DebugState = DebugAtCmd - break + debugLineInfo(m) case "s", "step": if m.DebugLoc != m.PrevDebugLoc { m.DebugState = DebugAtCmd m.PrevDebugLoc = m.DebugLoc debugList(m, "") - break + continue loop + } + default: + for _, b := range m.breakpoints { + if b == m.DebugLoc && m.DebugLoc != m.PrevDebugLoc { + m.DebugState = DebugAtCmd + m.PrevDebugLoc = m.DebugLoc + debugList(m, "") + continue loop + } } } break loop @@ -195,10 +212,101 @@ func debugUpdateLoc(m *Machine) { } } +// --------------------------------------- +const breakUsage = `break|b [locspec]` +const breakShort = `Set a breakpoint.` +const breakLong = ` +The syntax accepted for locspec is: +- : specifies the line in filename. Filename can be relative. +- specifies the line in the current source file. +- + specifies the line offset lines after the current one. +- - specifies the line offset lines before the current one. +` + +func debugBreak(m *Machine, arg string) error { + loc, err := arg2loc(m, arg) + if err != nil { + return err + } + m.breakpoints = append(m.breakpoints, loc) + fmt.Fprintf(m.DebugOut, "Breakpoint %d at %s %s:%d\n", len(m.breakpoints)-1, loc.PkgPath, loc.File, loc.Line) + return nil +} + +func arg2loc(m *Machine, arg string) (loc Location, err error) { + var filename string + var line int + + loc = m.DebugLoc + if strings.Contains(arg, ":") { + // Location is specified by filename:line or function:line + strs := strings.Split(arg, ":") + filename = strs[0] + if line, err = strconv.Atoi(strs[1]); err != nil { + return loc, err + } + if loc.File, err = filepath.Abs(filename); err != nil { + return loc, err + } + loc.PkgPath = m.DebugLoc.PkgPath + loc.File = path.Clean(loc.File) + loc.Line = line + return loc, nil + } + if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") { + // Location is specified as offset from current file. + if line, err = strconv.Atoi(arg); err != nil { + return + } + loc.Line += line + return loc, nil + } + if line, err = strconv.Atoi(arg); err == nil { + // Location is the line number in the current file. + loc.Line = line + return loc, nil + } + return +} + +// --------------------------------------- +const breakpointsUsage = `breakpoints|bp` +const breakpointsShort = `Print out info for active breakpoints.` + +func debugBreakpoints(m *Machine, arg string) error { + for i, b := range m.breakpoints { + fmt.Fprintf(m.DebugOut, "Breakpoint %d at %s %s:%d\n", i, b.PkgPath, b.File, b.Line) + } + return nil +} + +// --------------------------------------- +const clearUsage = `clear [id]` +const clearShort = `Delete breakpoint (all if no id).` + +func debugClear(m *Machine, arg string) error { + if arg != "" { + id, err := strconv.Atoi(arg) + if err != nil || id < 0 || id >= len(m.breakpoints) { + return fmt.Errorf("invalid breakpoint id: %v", arg) + } + m.breakpoints = append(m.breakpoints[:id], m.breakpoints[id+1:]...) + return nil + } + m.breakpoints = nil + return nil +} + // --------------------------------------- const continueUsage = `continue|c` const continueShort = `Run until breakpoint or program termination.` +const stepUsage = `step|s` +const stepShort = `Single step through program.` + +const stepiUsage = `stepi|si` +const stepiShort = `Single step a single VM instruction.` + func debugContinue(m *Machine, arg string) error { m.DebugState = DebugAtRun; return nil } // --------------------------------------- @@ -230,7 +338,7 @@ func debugHelp(m *Machine, arg string) error { if ok { t := fmt.Sprintf("%-25s %s", c.usage, c.short) if c.long != "" { - t += "\n\n" + c.long + t += "\n" + c.long } fmt.Fprintln(m.DebugOut, t) return nil @@ -247,7 +355,7 @@ func debugHelp(m *Machine, arg string) error { // --------------------------------------- const listUsage = `list|l` -const listShort = `Show source code` +const listShort = `Show source code.` func debugList(m *Machine, arg string) error { debugLineInfo(m) @@ -313,22 +421,3 @@ func debugPrint(m *Machine, arg string) error { println("not implemented yet") return nil } - -// --------------------------------------- -const stepUsage = `step|s` -const stepShort = `Single step through program.` - -func debugStep(m *Machine, arg string) error { - m.DebugState = DebugAtRun - return nil -} - -// --------------------------------------- -const stepiUsage = `stepi|si` -const stepiShort = `Single step a single VM instruction.` - -func debugStepi(m *Machine, arg string) error { - debugLineInfo(m) - m.DebugState = DebugAtRun - return nil -} From 8169086fae4cf13775c8815c316f579233cdd4ec Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 30 Jan 2024 16:07:28 +0100 Subject: [PATCH 06/37] Fix breakpoint setting with empty file. --- gnovm/pkg/gnolang/debugger.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 763327bee8d..c5d9975aec6 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -7,7 +7,6 @@ import ( "log" "net" "os" - "path" "path/filepath" "sort" "strconv" @@ -234,22 +233,21 @@ func debugBreak(m *Machine, arg string) error { } func arg2loc(m *Machine, arg string) (loc Location, err error) { - var filename string var line int - loc = m.DebugLoc + if strings.Contains(arg, ":") { // Location is specified by filename:line or function:line strs := strings.Split(arg, ":") - filename = strs[0] - if line, err = strconv.Atoi(strs[1]); err != nil { - return loc, err + if strs[0] != "" { + if loc.File, err = filepath.Abs(strs[0]); err != nil { + return loc, err + } + loc.File = filepath.Clean(loc.File) } - if loc.File, err = filepath.Abs(filename); err != nil { + if line, err = strconv.Atoi(strs[1]); err != nil { return loc, err } - loc.PkgPath = m.DebugLoc.PkgPath - loc.File = path.Clean(loc.File) loc.Line = line return loc, nil } From f74fd6153e749bd98cedb64cda9323c5a02c6236 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Wed, 31 Jan 2024 23:24:14 +0100 Subject: [PATCH 07/37] Implement 'stack' command to print the call stack. --- gnovm/pkg/gnolang/debugger.go | 81 +++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index c5d9975aec6..b7262a7b169 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -36,6 +36,7 @@ type Debugger struct { DebugLoc Location PrevDebugLoc Location breakpoints []Location + debugCall []Location // should be provided by machine frame } type debugCommand struct { @@ -58,8 +59,9 @@ func init() { "detach": {debugDetach, detachUsage, detachShort, ""}, "exit": {debugExit, exitUsage, exitShort, ""}, "help": {debugHelp, helpUsage, helpShort, ""}, - "list": {debugList, listUsage, listShort, ""}, + "list": {debugList, listUsage, listShort, listLong}, "print": {debugPrint, printUsage, printShort, ""}, + "stack": {debugStack, stackUsage, stackShort, ""}, "step": {debugContinue, stepUsage, stepShort, ""}, "stepi": {debugContinue, stepiUsage, stepiShort, ""}, } @@ -74,6 +76,7 @@ func init() { // Set command aliases. debugCmds["b"] = debugCmds["break"] debugCmds["bp"] = debugCmds["breakpoints"] + debugCmds["bt"] = debugCmds["stack"] debugCmds["c"] = debugCmds["continue"] debugCmds["h"] = debugCmds["help"] debugCmds["l"] = debugCmds["list"] @@ -126,6 +129,15 @@ loop: } } debugUpdateLoc(m) + + // Keep track of exact locations when performing calls. + op := m.Ops[m.NumOps-1] + switch op { + case OpCall: + m.debugCall = append(m.debugCall, m.DebugLoc) + case OpReturn: + m.debugCall = m.debugCall[:len(m.debugCall)-1] + } } // debugCmd processes a debugger REPL command. It displays a prompt, then @@ -183,6 +195,11 @@ func initDebugIO(m *Machine) { func debugUpdateLoc(m *Machine) { loc := m.LastBlock().Source.GetLocation() + // File must have an unambiguous absolute path. + if loc.File != "" && !filepath.IsAbs(loc.File) { + loc.File, _ = filepath.Abs(loc.File) + } + if m.DebugLoc.PkgPath == "" || loc.PkgPath != "" && loc.PkgPath != m.DebugLoc.PkgPath || loc.File != "" && loc.File != m.DebugLoc.File { @@ -237,7 +254,7 @@ func arg2loc(m *Machine, arg string) (loc Location, err error) { loc = m.DebugLoc if strings.Contains(arg, ":") { - // Location is specified by filename:line or function:line + // Location is specified by filename:line. strs := strings.Split(arg, ":") if strs[0] != "" { if loc.File, err = filepath.Abs(strs[0]); err != nil { @@ -252,9 +269,9 @@ func arg2loc(m *Machine, arg string) (loc Location, err error) { return loc, nil } if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") { - // Location is specified as offset from current file. + // Location is specified as a line offset from the current line. if line, err = strconv.Atoi(arg); err != nil { - return + return loc, err } loc.Line += line return loc, nil @@ -264,7 +281,7 @@ func arg2loc(m *Machine, arg string) (loc Location, err error) { loc.Line = line return loc, nil } - return + return loc, err } // --------------------------------------- @@ -352,35 +369,48 @@ func debugHelp(m *Machine, arg string) error { } // --------------------------------------- -const listUsage = `list|l` +const listUsage = `list|l [locspec]` const listShort = `Show source code.` +const listLong = ` +See 'help break' for locspec syntax. If locspec is empty, +list shows the source code around the current line. +` func debugList(m *Machine, arg string) error { - debugLineInfo(m) - lines, offset, err := sourceLines(m.DebugLoc.File, m.DebugLoc.Line) + file, line := m.DebugLoc.File, m.DebugLoc.Line + + if arg == "" { + debugLineInfo(m) + } else { + loc, err := arg2loc(m, arg) + if err != nil { + return err + } + file, line = loc.File, loc.Line + fmt.Fprintf(m.DebugOut, "Showing %s:%d\n", file, line) + } + lines, offset, err := sourceLines(file, line) if err != nil { return err } - for i, line := range lines { + for i, l := range lines { cursor := "" - if m.DebugLoc.Line == i+offset { + if file == m.DebugLoc.File && m.DebugLoc.Line == i+offset { cursor = "=>" } - fmt.Fprintf(m.DebugOut, "%2s %4d: %s\n", cursor, i+offset, line) + fmt.Fprintf(m.DebugOut, "%2s %4d: %s\n", cursor, i+offset, l) } return nil } func debugLineInfo(m *Machine) { line := string(m.Package.PkgName) - if len(m.Frames) > 0 { f := m.Frames[len(m.Frames)-1] if f.Func != nil { line += "." + string(f.Func.Name) + "()" } } - fmt.Fprintf(m.DebugOut, "> %s %s:%d\n", line, m.DebugLoc.File, m.DebugLoc.Line) } @@ -416,6 +446,29 @@ const printUsage = `print|p ` const printShort = `Print a variable or expression.` func debugPrint(m *Machine, arg string) error { - println("not implemented yet") + return errors.New("not yet implemented") +} + +// --------------------------------------- +const stackUsage = `stack|bt` +const stackShort = `Print stack trace.` + +func debugStack(m *Machine, arg string) error { + l := len(m.Frames) - 1 + // List frames from top to bottom. + for i := l; i >= 0; i-- { + f := m.Frames[i] + loc := debugFrameLoc(m, l-i) + t := fmt.Sprintf("%-3d in %s.%s\n at %s:%d", l-i, f.LastPackage.PkgPath, f.Func, loc.File, loc.Line) + fmt.Fprintln(m.DebugOut, t) + } return nil } + +func debugFrameLoc(m *Machine, n int) Location { + l := len(m.debugCall) - 1 + if loc := m.DebugLoc; l == n { + return loc + } + return m.debugCall[l-n] +} From 55d89ded89628e3b806da8aab94aa902059a5d86 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 1 Feb 2024 09:39:25 +0100 Subject: [PATCH 08/37] Improve stack trace --- gnovm/pkg/gnolang/debugger.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index b7262a7b169..0bb1c9ceeae 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -455,20 +455,18 @@ const stackShort = `Print stack trace.` func debugStack(m *Machine, arg string) error { l := len(m.Frames) - 1 - // List frames from top to bottom. + // List stack frames in reverse array order. Deepest level is 0. for i := l; i >= 0; i-- { f := m.Frames[i] loc := debugFrameLoc(m, l-i) - t := fmt.Sprintf("%-3d in %s.%s\n at %s:%d", l-i, f.LastPackage.PkgPath, f.Func, loc.File, loc.Line) - fmt.Fprintln(m.DebugOut, t) + fmt.Fprintf(m.DebugOut, "%d\tin %s.%s\n\tat %s:%d\n", l-i, f.LastPackage.PkgPath, f.Func, loc.File, loc.Line) } return nil } func debugFrameLoc(m *Machine, n int) Location { - l := len(m.debugCall) - 1 - if loc := m.DebugLoc; l == n { - return loc + if n == 0 || len(m.debugCall) == 0 { + return m.DebugLoc } - return m.debugCall[l-n] + return m.debugCall[len(m.debugCall)-n] } From f5e13d775fd4f1d19e09c21248798a2667c5121e Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 1 Feb 2024 11:22:18 +0100 Subject: [PATCH 09/37] Implement 'up' and 'down' command to navigate the call stack. --- gnovm/pkg/gnolang/debugger.go | 72 ++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 0bb1c9ceeae..2a4dc93b3f2 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -31,12 +31,13 @@ type Debugger struct { DebugIn io.ReadCloser DebugOut io.Writer - lastDebugCmd string - lastDebugArg string - DebugLoc Location - PrevDebugLoc Location - breakpoints []Location - debugCall []Location // should be provided by machine frame + lastDebugCmd string + lastDebugArg string + DebugLoc Location + PrevDebugLoc Location + breakpoints []Location + debugCall []Location // should be provided by machine frame + debugFrameLevel int } type debugCommand struct { @@ -57,6 +58,7 @@ func init() { "clear": {debugClear, clearUsage, clearShort, ""}, "continue": {debugContinue, continueUsage, continueShort, ""}, "detach": {debugDetach, detachUsage, detachShort, ""}, + "down": {debugDown, downUsage, downShort, ""}, "exit": {debugExit, exitUsage, exitShort, ""}, "help": {debugHelp, helpUsage, helpShort, ""}, "list": {debugList, listUsage, listShort, listLong}, @@ -64,6 +66,7 @@ func init() { "stack": {debugStack, stackUsage, stackShort, ""}, "step": {debugContinue, stepUsage, stepShort, ""}, "stepi": {debugContinue, stepiUsage, stepiShort, ""}, + "up": {debugUp, upUsage, upShort, ""}, } // Sort command names for help. @@ -135,7 +138,7 @@ loop: switch op { case OpCall: m.debugCall = append(m.debugCall, m.DebugLoc) - case OpReturn: + case OpReturn, OpReturnFromBlock: m.debugCall = m.debugCall[:len(m.debugCall)-1] } } @@ -335,6 +338,24 @@ func debugDetach(m *Machine, arg string) error { return nil } +// --------------------------------------- +const downUsage = `down [n]` +const downShort = `Move the current frame down by n (default 1).` + +func debugDown(m *Machine, arg string) (err error) { + n := 1 + if arg != "" { + if n, err = strconv.Atoi(arg); err != nil { + return err + } + } + if level := m.debugFrameLevel - n; level >= 0 && level < len(m.debugCall) { + m.debugFrameLevel = level + } + debugList(m, "") + return nil +} + // --------------------------------------- const exitUsage = `exit|quit|q` const exitShort = `Exit the debugger and program.` @@ -376,26 +397,31 @@ See 'help break' for locspec syntax. If locspec is empty, list shows the source code around the current line. ` -func debugList(m *Machine, arg string) error { - file, line := m.DebugLoc.File, m.DebugLoc.Line +func debugList(m *Machine, arg string) (err error) { + loc := m.DebugLoc + hideCursor := false if arg == "" { debugLineInfo(m) + if m.lastDebugCmd == "up" || m.lastDebugCmd == "down" { + loc = debugFrameLoc(m, m.debugFrameLevel) + fmt.Fprintf(m.DebugOut, "Frame %d: %s:%d\n", m.debugFrameLevel, loc.File, loc.Line) + } } else { - loc, err := arg2loc(m, arg) - if err != nil { + if loc, err = arg2loc(m, arg); err != nil { return err } - file, line = loc.File, loc.Line - fmt.Fprintf(m.DebugOut, "Showing %s:%d\n", file, line) + hideCursor = true + fmt.Fprintf(m.DebugOut, "Showing %s:%d\n", loc.File, loc.Line) } + file, line := loc.File, loc.Line lines, offset, err := sourceLines(file, line) if err != nil { return err } for i, l := range lines { cursor := "" - if file == m.DebugLoc.File && m.DebugLoc.Line == i+offset { + if !hideCursor && file == loc.File && loc.Line == i+offset { cursor = "=>" } fmt.Fprintf(m.DebugOut, "%2s %4d: %s\n", cursor, i+offset, l) @@ -470,3 +496,21 @@ func debugFrameLoc(m *Machine, n int) Location { } return m.debugCall[len(m.debugCall)-n] } + +// --------------------------------------- +const upUsage = `up [n]` +const upShort = `Move the current frame up by n (default 1).` + +func debugUp(m *Machine, arg string) (err error) { + n := 1 + if arg != "" { + if n, err = strconv.Atoi(arg); err != nil { + return err + } + } + if level := m.debugFrameLevel + n; level >= 0 && level < len(m.debugCall) { + m.debugFrameLevel = level + } + debugList(m, "") + return nil +} From 071b4be193a0bd7c01c99cfd3c87dcb70e2a9bb9 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 1 Feb 2024 14:26:37 +0100 Subject: [PATCH 10/37] Implement 'print' command to print a symbol value. --- gnovm/pkg/gnolang/debugger.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 2a4dc93b3f2..a370421da78 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -325,7 +325,11 @@ const stepShort = `Single step through program.` const stepiUsage = `stepi|si` const stepiShort = `Single step a single VM instruction.` -func debugContinue(m *Machine, arg string) error { m.DebugState = DebugAtRun; return nil } +func debugContinue(m *Machine, arg string) error { + m.DebugState = DebugAtRun + m.debugFrameLevel = 0 + return nil +} // --------------------------------------- const detachUsage = `detach` @@ -472,7 +476,18 @@ const printUsage = `print|p ` const printShort = `Print a variable or expression.` func debugPrint(m *Machine, arg string) error { - return errors.New("not yet implemented") + if arg == "" { + return errors.New("missing argument") + } + b := m.Blocks[len(m.Blocks)-1-m.debugFrameLevel] + for i, name := range b.Source.GetBlockNames() { + // TODO: handle index and selector expressions. + if string(name) == arg { + fmt.Fprintln(m.DebugOut, b.Values[i]) + return nil + } + } + return fmt.Errorf("could not find symbol value for %s", arg) } // --------------------------------------- From 2a64081eb2724988712bb913a0ea97da1c369f76 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 1 Feb 2024 14:47:09 +0100 Subject: [PATCH 11/37] Remove dependency on log package. --- gnovm/pkg/gnolang/debugger.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index a370421da78..f4f96355feb 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "log" "net" "os" "path/filepath" @@ -49,8 +48,6 @@ var debugCmds map[string]debugCommand var debugCmdNames []string func init() { - log.SetFlags(log.Lshortfile) - // Register debugger commands. debugCmds = map[string]debugCommand{ "break": {debugBreak, breakUsage, breakShort, breakLong}, From a7c387647c6886591ea78192c42d7960da8ab19e Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 2 Feb 2024 10:35:37 +0100 Subject: [PATCH 12/37] Improve comment and function naming to address feedback from @go7066 --- gnovm/pkg/gnolang/debugger.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index f4f96355feb..46953a4cb45 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -25,11 +25,11 @@ const ( // Debugger describes a machine debugger state. type Debugger struct { DebugEnabled bool - DebugAddr string - DebugState - DebugIn io.ReadCloser - DebugOut io.Writer + DebugAddr string // optional address [host]:port for DebugIn/DebugOut + DebugIn io.ReadCloser // debugger input, defaults to Stdin + DebugOut io.Writer // debugger output, defaults to Stdout + DebugState lastDebugCmd string lastDebugArg string DebugLoc Location @@ -94,7 +94,7 @@ loop: switch m.DebugState { case DebugAtInit: initDebugIO(m) - debugUpdateLoc(m) + debugUpdateLocation(m) fmt.Fprintln(m.DebugOut, "Welcome to the Gnovm debugger. Type 'help' for list of commands.") m.DebugState = DebugAtCmd case DebugAtCmd: @@ -128,7 +128,7 @@ loop: os.Exit(0) } } - debugUpdateLoc(m) + debugUpdateLocation(m) // Keep track of exact locations when performing calls. op := m.Ops[m.NumOps-1] @@ -190,9 +190,9 @@ func initDebugIO(m *Machine) { m.DebugOut = conn } -// debugUpdateLoc computes the source code location for the current VM state. +// debugUpdateLocation computes the source code location for the current VM state. // The result is stored in Debugger.DebugLoc. -func debugUpdateLoc(m *Machine) { +func debugUpdateLocation(m *Machine) { loc := m.LastBlock().Source.GetLocation() // File must have an unambiguous absolute path. From c30279010ba3e2fe919711b6bd1962e575fb007f Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 6 Feb 2024 18:13:14 +0100 Subject: [PATCH 13/37] Add unit tests for debugger --- gnovm/cmd/gno/debug_test.go | 56 +++++++++++++++++++++++++++ gnovm/cmd/gno/run.go | 2 + gnovm/pkg/gnolang/debugger.go | 17 ++++---- gnovm/tests/integ/debugger/sample.gno | 18 +++++++++ 4 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 gnovm/cmd/gno/debug_test.go create mode 100644 gnovm/tests/integ/debugger/sample.gno diff --git a/gnovm/cmd/gno/debug_test.go b/gnovm/cmd/gno/debug_test.go new file mode 100644 index 00000000000..5f4266f2c50 --- /dev/null +++ b/gnovm/cmd/gno/debug_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type dtest struct{ in, out string } + +const debugTarget = "../../tests/integ/debugger/sample.gno" + +func runDebugTest(t *testing.T, tests []dtest) { + args := []string{"run", "-debug", debugTarget} + + for _, test := range tests { + test := test + t.Run("", func(t *testing.T) { + out := bytes.NewBufferString("") + err := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetIn(bytes.NewBufferString(test.in)) + io.SetOut(commands.WriteNopCloser(out)) + io.SetErr(commands.WriteNopCloser(err)) + if err := newGnocliCmd(io).ParseAndRun(context.Background(), args); err != nil { + t.Fatal(err) + } + t.Log("out:", out) + if !strings.Contains(out.String(), test.out) { + t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out.String()) + } + }) + } +} + +func TestDebug(t *testing.T) { + brk := "break " + debugTarget + ":7\n" + cont := brk + "continue\n" + + runDebugTest(t, []dtest{ + {in: "", out: "Welcome to the Gnovm debugger. Type 'help' for list of commands."}, + {in: "help\n", out: "The following commands are available"}, + {in: "h\n", out: "The following commands are available"}, + {in: "help h\n", out: "Print the help message."}, + {in: "list " + debugTarget + ":1\n", out: "1: // This is a sample target"}, + {in: brk, out: "Breakpoint 0 at main "}, + {in: cont, out: "=> 7: println(name, i)"}, + {in: cont + "stack\n", out: "2 in main.main"}, + {in: cont + "up\n", out: "=> 11: f(s, n)"}, + {in: cont + "print name\n", out: `("hello" string)`}, + {in: cont + "p i\n", out: `(3 int)`}, + }) +} diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index cd964905254..a6fc09a8b56 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -118,6 +118,8 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { Debug: cfg.debug || cfg.debugAddr != "", DebugAddr: cfg.debugAddr, }) + m.DebugIn = stdin + m.DebugOut = stdout defer m.Release() diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 46953a4cb45..78b87b4830c 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -25,9 +25,9 @@ const ( // Debugger describes a machine debugger state. type Debugger struct { DebugEnabled bool - DebugAddr string // optional address [host]:port for DebugIn/DebugOut - DebugIn io.ReadCloser // debugger input, defaults to Stdin - DebugOut io.Writer // debugger output, defaults to Stdout + DebugAddr string // optional address [host]:port for DebugIn/DebugOut + DebugIn io.Reader // debugger input, defaults to Stdin + DebugOut io.Writer // debugger output, defaults to Stdout DebugState lastDebugCmd string @@ -172,8 +172,6 @@ func debugCmd(m *Machine) error { // An error during connection setting will result in program panic. func initDebugIO(m *Machine) { if m.DebugAddr == "" { - m.DebugIn = os.Stdin - m.DebugOut = os.Stdout return } l, err := net.Listen("tcp", m.DebugAddr) @@ -335,7 +333,9 @@ const detachShort = `Close debugger and resume program.` func debugDetach(m *Machine, arg string) error { m.DebugEnabled = false m.DebugState = DebugAtRun - m.DebugIn.Close() + if i, ok := m.DebugIn.(io.Closer); ok { + i.Close() + } return nil } @@ -380,7 +380,7 @@ func debugHelp(m *Machine, arg string) error { fmt.Fprintln(m.DebugOut, t) return nil } - t := "The followings commands are available:\n\n" + t := "The following commands are available:\n\n" for _, name := range debugCmdNames { c := debugCmds[name] t += fmt.Sprintf("%-25s %s\n", c.usage, c.short) @@ -496,6 +496,9 @@ func debugStack(m *Machine, arg string) error { // List stack frames in reverse array order. Deepest level is 0. for i := l; i >= 0; i-- { f := m.Frames[i] + if f.Func == nil { + continue + } loc := debugFrameLoc(m, l-i) fmt.Fprintf(m.DebugOut, "%d\tin %s.%s\n\tat %s:%d\n", l-i, f.LastPackage.PkgPath, f.Func, loc.File, loc.Line) } diff --git a/gnovm/tests/integ/debugger/sample.gno b/gnovm/tests/integ/debugger/sample.gno new file mode 100644 index 00000000000..b52add05056 --- /dev/null +++ b/gnovm/tests/integ/debugger/sample.gno @@ -0,0 +1,18 @@ +// This is a sample target gno program to test the gnovm debugger. +// See ../../cmd/gno/debug_test.go for the debugger test cases. + +package main + +func f(name string, i int) { + println(name, i) +} + +func g(s string, n int) { + f(s, n) +} + +func main() { + println("before f") + g("hello", 3) + println("bye") +} From 66ace69b45daba56a9d865379a3216c4ed67bfe5 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 9 Feb 2024 18:18:42 +0100 Subject: [PATCH 14/37] Fix scope issues when for stack and print commands. --- gnovm/cmd/gno/debug_test.go | 2 +- gnovm/pkg/gnolang/debugger.go | 91 +++++++++++++++++++++++---- gnovm/tests/integ/debugger/sample.gno | 9 ++- 3 files changed, 88 insertions(+), 14 deletions(-) diff --git a/gnovm/cmd/gno/debug_test.go b/gnovm/cmd/gno/debug_test.go index 5f4266f2c50..828ddd9f54c 100644 --- a/gnovm/cmd/gno/debug_test.go +++ b/gnovm/cmd/gno/debug_test.go @@ -28,7 +28,7 @@ func runDebugTest(t *testing.T, tests []dtest) { if err := newGnocliCmd(io).ParseAndRun(context.Background(), args); err != nil { t.Fatal(err) } - t.Log("out:", out) + t.Log("in:", test.in, "out:", out) if !strings.Contains(out.String(), test.out) { t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out.String()) } diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 78b87b4830c..3f57ab0c611 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -476,12 +476,61 @@ func debugPrint(m *Machine, arg string) error { if arg == "" { return errors.New("missing argument") } - b := m.Blocks[len(m.Blocks)-1-m.debugFrameLevel] - for i, name := range b.Source.GetBlockNames() { - // TODO: handle index and selector expressions. - if string(name) == arg { - fmt.Fprintln(m.DebugOut, b.Values[i]) - return nil + // TODO: parse expression. + + // Position to the right frame. + ncall := 0 + var i int + var fblocks []BlockNode + var funBlock BlockNode + for i = len(m.Frames) - 1; i >= 0; i-- { + if m.Frames[i].Func != nil { + funBlock = m.Frames[i].Func.Source + } + if ncall == m.debugFrameLevel { + break + } + if m.Frames[i].Func != nil { + fblocks = append(fblocks, m.Frames[i].Func.Source) + ncall++ + } + } + if i < 0 { + return fmt.Errorf("invalid frame level: %d", m.debugFrameLevel) + } + + // Position to the right block, i.e the first after the last fblock (if any). + for i = len(m.Blocks) - 1; i >= 0; i-- { + if len(fblocks) == 0 { + break + } + if m.Blocks[i].Source == fblocks[0] { + fblocks = fblocks[1:] + } + } + if i < 0 { + return fmt.Errorf("invalid block level: %d", m.debugFrameLevel) + } + + // list SourceBlocks in the same frame level. + var sblocks []*Block + for ; i >= 0; i-- { + sblocks = append(sblocks, m.Blocks[i]) + if m.Blocks[i].Source == funBlock { + break + } + } + if i > 0 { + sblocks = append(sblocks, m.Blocks[0]) // Add global block + } + + // Search value if current frame level blocks, or main. + for _, b := range sblocks { + for i, name := range b.Source.GetBlockNames() { + if string(name) == arg { + fmt.Fprintln(m.DebugOut, b.Values[i]) + return nil + } } } return fmt.Errorf("could not find symbol value for %s", arg) @@ -492,15 +541,35 @@ const stackUsage = `stack|bt` const stackShort = `Print stack trace.` func debugStack(m *Machine, arg string) error { - l := len(m.Frames) - 1 - // List stack frames in reverse array order. Deepest level is 0. - for i := l; i >= 0; i-- { + i := 0 + for { + ff := debugFrameFunc(m, i) + loc := debugFrameLoc(m, i) + if ff == nil { + break + } + var fname string + if ff.IsMethod { + fname = fmt.Sprintf("%v.(%v).%v", ff.PkgPath, ff.Type.(*FuncType).Params[0].Type, ff.Name) + } else { + fname = fmt.Sprintf("%v.%v", ff.PkgPath, ff.Name) + } + fmt.Fprintf(m.DebugOut, "%d\tin %s\n\tat %s:%d\n", i, fname, loc.File, loc.Line) + i++ + } + return nil +} + +func debugFrameFunc(m *Machine, n int) *FuncValue { + for ncall, i := 0, len(m.Frames)-1; i >= 0; i-- { f := m.Frames[i] if f.Func == nil { continue } - loc := debugFrameLoc(m, l-i) - fmt.Fprintf(m.DebugOut, "%d\tin %s.%s\n\tat %s:%d\n", l-i, f.LastPackage.PkgPath, f.Func, loc.File, loc.Line) + if ncall == n { + return f.Func + } + ncall++ } return nil } diff --git a/gnovm/tests/integ/debugger/sample.gno b/gnovm/tests/integ/debugger/sample.gno index b52add05056..06a689a78a3 100644 --- a/gnovm/tests/integ/debugger/sample.gno +++ b/gnovm/tests/integ/debugger/sample.gno @@ -11,8 +11,13 @@ func g(s string, n int) { f(s, n) } +var global = "test" + func main() { - println("before f") - g("hello", 3) + num := 5 + println("in main") + if num > 2 { + g("hello", 3) + } println("bye") } From 3976c5748af6ea5ac642b170307aab60840a8193 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 16 Feb 2024 14:01:41 +0100 Subject: [PATCH 15/37] Implement a go expression parser for the 'print' command. debugEvalExpr evaluates a Go expression in the context of the VM and returns the corresponding value, or an error. The supported expression syntax is a small subset of Go expressions: basic literals, identifiers, selectors, index expressions, or a combination of those are supported, but none of function calls, arithmetic, logic or assign operations, type assertions of convertions. --- gnovm/pkg/gnolang/debugger.go | 109 ++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 3f57ab0c611..86abd7b469b 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -3,6 +3,9 @@ package gnolang import ( "errors" "fmt" + "go/ast" + "go/parser" + "go/token" "io" "net" "os" @@ -472,12 +475,97 @@ func min(a, b int) int { const printUsage = `print|p ` const printShort = `Print a variable or expression.` -func debugPrint(m *Machine, arg string) error { +func debugPrint(m *Machine, arg string) (err error) { if arg == "" { return errors.New("missing argument") } - // TODO: parse expression. + // Use the Go parser to get the AST representation of print argument as a Go expresssion. + ast, err := parser.ParseExpr(arg) + if err != nil { + return err + } + tv, err := debugEvalExpr(m, ast) + if err != nil { + return err + } + fmt.Println(m.DebugOut, tv) + return nil +} + +// debugEvalExpr evaluates a Go expression in the context of the VM and returns +// the corresponding value, or an error. +// The supported expression syntax is a small subset of Go expressions: +// basic literals, identifiers, selectors, index expressions, or a combination +// of those are supported, but none of function calls, arithmetic, logic or +// assign operations, type assertions of convertions. +// This is sufficient for a debugger to perform 'print (*f).S[x][y]' for example. +func debugEvalExpr(m *Machine, node ast.Node) (tv TypedValue, err error) { + switch n := node.(type) { + case *ast.BasicLit: + switch n.Kind { + case token.INT: + i, err := strconv.ParseInt(n.Value, 0, 0) + if err != nil { + return tv, err + } + return typedInt(int(i)), nil + case token.CHAR: + return typedRune(([]rune(n.Value))[0]), nil + case token.STRING: + return typedString(n.Value), nil + } + return tv, fmt.Errorf("invalid basic literal value: %s", n.Value) + case *ast.Ident: + if tv, ok := debugLookup(m, n.Name); ok { + return tv, nil + } + return tv, fmt.Errorf("could not find symbol value for %s", n.Name) + case *ast.ParenExpr: + return debugEvalExpr(m, n.X) + case *ast.StarExpr: + x, err := debugEvalExpr(m, n.X) + if err != nil { + return tv, err + } + pv, ok := x.V.(PointerValue) + if !ok { + return tv, fmt.Errorf("Not a pointer value: %v", x) + } + return pv.Deref(), nil + case *ast.SelectorExpr: + x, err := debugEvalExpr(m, n.X) + if err != nil { + return tv, err + } + // TODO: handle selector on package. + tr, _, _, _, _ := findEmbeddedFieldType(x.T.GetPkgPath(), x.T, Name(n.Sel.Name), nil) + if len(tr) == 0 { + return tv, fmt.Errorf("invalid selector: %s", n.Sel.Name) + } + for _, vp := range tr { + x = x.GetPointerTo(m.Alloc, m.Store, vp).Deref() + } + return x, nil + case *ast.IndexExpr: + x, err := debugEvalExpr(m, n.X) + if err != nil { + return tv, err + } + index, err := debugEvalExpr(m, n.Index) + if err != nil { + return tv, err + } + return x.GetPointerAtIndex(m.Alloc, m.Store, &index).Deref(), nil + default: + err = fmt.Errorf("expression not supported: %v", n) + } + return tv, err +} +// debugLookup returns the current VM value corresponding to name ident in +// the current function call frame, or the global frame if not found. +// Note: the commands 'up' and 'down' change the frame level to start from. +func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { // Position to the right frame. ncall := 0 var i int @@ -496,7 +584,7 @@ func debugPrint(m *Machine, arg string) error { } } if i < 0 { - return fmt.Errorf("invalid frame level: %d", m.debugFrameLevel) + return tv, false } // Position to the right block, i.e the first after the last fblock (if any). @@ -509,10 +597,10 @@ func debugPrint(m *Machine, arg string) error { } } if i < 0 { - return fmt.Errorf("invalid block level: %d", m.debugFrameLevel) + return tv, false } - // list SourceBlocks in the same frame level. + // get SourceBlocks in the same frame level. var sblocks []*Block for ; i >= 0; i-- { sblocks = append(sblocks, m.Blocks[i]) @@ -524,16 +612,15 @@ func debugPrint(m *Machine, arg string) error { sblocks = append(sblocks, m.Blocks[0]) // Add global block } - // Search value if current frame level blocks, or main. + // Search value in current frame level blocks, or main. for _, b := range sblocks { - for i, name := range b.Source.GetBlockNames() { - if string(name) == arg { - fmt.Fprintln(m.DebugOut, b.Values[i]) - return nil + for i, s := range b.Source.GetBlockNames() { + if string(s) == name { + return b.Values[i], true } } } - return fmt.Errorf("could not find symbol value for %s", arg) + return tv, false } // --------------------------------------- From 0ac45cbe0533f7b1c9d39f0960a57e79ff604f7e Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Sun, 18 Feb 2024 17:05:29 +0100 Subject: [PATCH 16/37] Harden line scanning. Ignore lines starting by '#' (comments). --- gnovm/pkg/gnolang/debugger.go | 39 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 86abd7b469b..edf3e0249e0 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -1,6 +1,7 @@ package gnolang import ( + "bufio" "errors" "fmt" "go/ast" @@ -31,6 +32,7 @@ type Debugger struct { DebugAddr string // optional address [host]:port for DebugIn/DebugOut DebugIn io.Reader // debugger input, defaults to Stdin DebugOut io.Writer // debugger output, defaults to Stdout + DebugScanner *bufio.Scanner DebugState lastDebugCmd string @@ -150,13 +152,18 @@ loop: func debugCmd(m *Machine) error { var cmd, arg string fmt.Fprint(m.DebugOut, "dbg> ") - if n, err := fmt.Fscanln(m.DebugIn, &cmd, &arg); errors.Is(err, io.EOF) { + if !m.DebugScanner.Scan() { return debugDetach(m, arg) // Clean close of debugger, the target program resumes. - } else if n == 0 { + } + line := m.DebugScanner.Text() + n, _ := fmt.Sscan(line, &cmd, &arg) + if n == 0 { if m.lastDebugCmd == "" { return nil } cmd, arg = m.lastDebugCmd, m.lastDebugArg + } else if cmd[0] == '#' { + return nil } c, ok := debugCmds[cmd] if !ok { @@ -174,21 +181,21 @@ func debugCmd(m *Machine) error { // not affecting the target program's. // An error during connection setting will result in program panic. func initDebugIO(m *Machine) { - if m.DebugAddr == "" { - return - } - l, err := net.Listen("tcp", m.DebugAddr) - if err != nil { - panic(err) - } - print("Waiting for debugger client to connect at ", m.DebugAddr) - conn, err := l.Accept() - if err != nil { - panic(err) + if m.DebugAddr != "" { + l, err := net.Listen("tcp", m.DebugAddr) + if err != nil { + panic(err) + } + print("Waiting for debugger client to connect at ", m.DebugAddr) + conn, err := l.Accept() + if err != nil { + panic(err) + } + println(" connected!") + m.DebugIn = conn + m.DebugOut = conn } - println(" connected!") - m.DebugIn = conn - m.DebugOut = conn + m.DebugScanner = bufio.NewScanner(m.DebugIn) } // debugUpdateLocation computes the source code location for the current VM state. From 7455761db85575f5ea4019d8879b519241ef0c3d Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 28 Mar 2024 17:08:36 +0100 Subject: [PATCH 17/37] simplify debug command names sorting Co-authored-by: Morgan --- gnovm/pkg/gnolang/debugger.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index edf3e0249e0..4717acd20e0 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -72,11 +72,7 @@ func init() { } // Sort command names for help. - debugCmdNames = make([]string, 0, len(debugCmds)) - for name := range debugCmds { - debugCmdNames = append(debugCmdNames, name) - } - sort.SliceStable(debugCmdNames, func(i, j int) bool { return debugCmdNames[i] < debugCmdNames[j] }) + debugCmdNames = sort.Strings(maps.Keys(debugCmds)) // Set command aliases. debugCmds["b"] = debugCmds["break"] From 49149ccd653e579290b0ba8f8dfa46cc212676f0 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 28 Mar 2024 17:23:11 +0100 Subject: [PATCH 18/37] Revert "simplify debug command names sorting" This reverts commit 7455761db85575f5ea4019d8879b519241ef0c3d. --- gnovm/pkg/gnolang/debugger.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 4717acd20e0..edf3e0249e0 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -72,7 +72,11 @@ func init() { } // Sort command names for help. - debugCmdNames = sort.Strings(maps.Keys(debugCmds)) + debugCmdNames = make([]string, 0, len(debugCmds)) + for name := range debugCmds { + debugCmdNames = append(debugCmdNames, name) + } + sort.SliceStable(debugCmdNames, func(i, j int) bool { return debugCmdNames[i] < debugCmdNames[j] }) // Set command aliases. debugCmds["b"] = debugCmds["break"] From 11f51dee37fc50ef77ea4ae8688158ee1b3a594d Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 4 Apr 2024 12:30:14 +0200 Subject: [PATCH 19/37] Address review feedback. No semantic change. --- gnovm/cmd/gno/debug_test.go | 4 +- gnovm/cmd/gno/run.go | 3 +- gnovm/pkg/gnolang/debugger.go | 283 +++++++++++++++++++--------------- gnovm/pkg/gnolang/machine.go | 11 +- 4 files changed, 172 insertions(+), 129 deletions(-) diff --git a/gnovm/cmd/gno/debug_test.go b/gnovm/cmd/gno/debug_test.go index 828ddd9f54c..066cac73222 100644 --- a/gnovm/cmd/gno/debug_test.go +++ b/gnovm/cmd/gno/debug_test.go @@ -28,7 +28,7 @@ func runDebugTest(t *testing.T, tests []dtest) { if err := newGnocliCmd(io).ParseAndRun(context.Background(), args); err != nil { t.Fatal(err) } - t.Log("in:", test.in, "out:", out) + t.Log("in:", test.in, "out:", out, "err:", err) if !strings.Contains(out.String(), test.out) { t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out.String()) } @@ -37,7 +37,7 @@ func runDebugTest(t *testing.T, tests []dtest) { } func TestDebug(t *testing.T) { - brk := "break " + debugTarget + ":7\n" + brk := "break 7\n" cont := brk + "continue\n" runDebugTest(t, []dtest{ diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index a6fc09a8b56..c4fc9b2825b 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -113,13 +113,12 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { m := gno.NewMachineWithOptions(gno.MachineOptions{ PkgPath: string(files[0].PkgName), + Input: stdin, Output: stdout, Store: testStore, Debug: cfg.debug || cfg.debugAddr != "", DebugAddr: cfg.debugAddr, }) - m.DebugIn = stdin - m.DebugOut = stdout defer m.Release() diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index edf3e0249e0..c5e7a64418e 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -16,32 +16,41 @@ import ( "strings" ) -// DebugState is the state of the machine debugger. +// DebugState is the state of the machine debugger, defined by a finite state +// automaton with the following transitions, evaluated at each debugger input +// or each gnoVM instruction while in step mode: +// - DebugAtInit -> DebugAtCmd: initial debugger setup is performed +// - DebugAtCmd -> DebugAtCmd: when command is for inspecting or setting a breakpoint +// - DebugAtCmd -> DebuAtRun: when command is 'continue', 'step' or 'stepi' +// - DebugAtCmd -> DebugAtExit: when command is 'quit' or 'resume' +// - DebugAtRun -> DebugAtRun: when current machine instruction doesn't match a breakpoint +// - DebugAtRun -> DebugAtCmd: when current machine instruction matches a breakpoint +// - DebugAtRun -> DebugAtExit: when the program terminates type DebugState int const ( - DebugAtInit DebugState = iota - DebugAtCmd - DebugAtRun - DebugAtExit + DebugAtInit DebugState = iota // performs debugger IO setup and enters gnoVM in step mode + DebugAtCmd // awaits a new command from the debugger input stream + DebugAtRun // awaits the next machine instruction + DebugAtExit // closes debugger IO and exits gnoVM from step mode ) -// Debugger describes a machine debugger state. +// Debugger describes a machine debugger. type Debugger struct { - DebugEnabled bool - DebugAddr string // optional address [host]:port for DebugIn/DebugOut - DebugIn io.Reader // debugger input, defaults to Stdin - DebugOut io.Writer // debugger output, defaults to Stdout - DebugScanner *bufio.Scanner - - DebugState - lastDebugCmd string - lastDebugArg string - DebugLoc Location - PrevDebugLoc Location - breakpoints []Location - debugCall []Location // should be provided by machine frame - debugFrameLevel int + enabled bool // when true, machine is in step mode + addr string // optional network address [host]:port for In/Out + in io.Reader // debugger input, defaults to Stdin + out io.Writer // debugger output, defaults to Stdout + scanner *bufio.Scanner // to parse input per line + + state DebugState // current state of debugger + lastCmd string // last debugger command + lastArg string // last debugger command arguments + loc Location // source location of the current machine instruction + prevLoc Location // source location of the previous machine instruction + breakpoints []Location // list of breakpoints set by user, as source locations + call []Location // for function tracking, ideally should be provided by machine frame + frameLevel int // frame level of the current machine instruction } type debugCommand struct { @@ -49,8 +58,10 @@ type debugCommand struct { usage, short, long string // command help texts } -var debugCmds map[string]debugCommand -var debugCmdNames []string +var ( + debugCmds map[string]debugCommand + debugCmdNames []string +) func init() { // Register debugger commands. @@ -66,9 +77,11 @@ func init() { "list": {debugList, listUsage, listShort, listLong}, "print": {debugPrint, printUsage, printShort, ""}, "stack": {debugStack, stackUsage, stackShort, ""}, - "step": {debugContinue, stepUsage, stepShort, ""}, - "stepi": {debugContinue, stepiUsage, stepiShort, ""}, - "up": {debugUp, upUsage, upShort, ""}, + // NOTE: the difference between continue, step and stepi is handled within + // the main Debug() loop.:w + "step": {debugContinue, stepUsage, stepShort, ""}, + "stepi": {debugContinue, stepiUsage, stepiShort, ""}, + "up": {debugUp, upUsage, upShort, ""}, } // Sort command names for help. @@ -92,37 +105,37 @@ func init() { debugCmds["si"] = debugCmds["stepi"] } -// Debug is the debug callback invoked at each VM execution step. +// Debug is the debug callback invoked at each VM execution step. It implements the DebugState FSA. func (m *Machine) Debug() { loop: for { - switch m.DebugState { + switch m.Debugger.state { case DebugAtInit: initDebugIO(m) debugUpdateLocation(m) - fmt.Fprintln(m.DebugOut, "Welcome to the Gnovm debugger. Type 'help' for list of commands.") - m.DebugState = DebugAtCmd + fmt.Fprintln(m.Debugger.out, "Welcome to the Gnovm debugger. Type 'help' for list of commands.") + m.Debugger.state = DebugAtCmd case DebugAtCmd: if err := debugCmd(m); err != nil { - fmt.Fprintln(m.DebugOut, "Command failed:", err) + fmt.Fprintln(m.Debugger.out, "Command failed:", err) } case DebugAtRun: - switch m.lastDebugCmd { + switch m.Debugger.lastCmd { case "si", "stepi": - m.DebugState = DebugAtCmd + m.Debugger.state = DebugAtCmd debugLineInfo(m) case "s", "step": - if m.DebugLoc != m.PrevDebugLoc { - m.DebugState = DebugAtCmd - m.PrevDebugLoc = m.DebugLoc + if m.Debugger.loc != m.Debugger.prevLoc { + m.Debugger.state = DebugAtCmd + m.Debugger.prevLoc = m.Debugger.loc debugList(m, "") continue loop } default: for _, b := range m.breakpoints { - if b == m.DebugLoc && m.DebugLoc != m.PrevDebugLoc { - m.DebugState = DebugAtCmd - m.PrevDebugLoc = m.DebugLoc + if b == m.Debugger.loc && m.Debugger.loc != m.Debugger.prevLoc { + m.Debugger.state = DebugAtCmd + m.Debugger.prevLoc = m.Debugger.loc debugList(m, "") continue loop } @@ -139,9 +152,9 @@ loop: op := m.Ops[m.NumOps-1] switch op { case OpCall: - m.debugCall = append(m.debugCall, m.DebugLoc) + m.Debugger.call = append(m.Debugger.call, m.Debugger.loc) case OpReturn, OpReturnFromBlock: - m.debugCall = m.debugCall[:len(m.debugCall)-1] + m.Debugger.call = m.Debugger.call[:len(m.Debugger.call)-1] } } @@ -151,17 +164,17 @@ loop: // If the command is empty, the last non-empty command is repeated. func debugCmd(m *Machine) error { var cmd, arg string - fmt.Fprint(m.DebugOut, "dbg> ") - if !m.DebugScanner.Scan() { + fmt.Fprint(m.Debugger.out, "dbg> ") + if !m.Debugger.scanner.Scan() { return debugDetach(m, arg) // Clean close of debugger, the target program resumes. } - line := m.DebugScanner.Text() + line := m.Debugger.scanner.Text() n, _ := fmt.Sscan(line, &cmd, &arg) if n == 0 { - if m.lastDebugCmd == "" { + if m.Debugger.lastCmd == "" { return nil } - cmd, arg = m.lastDebugCmd, m.lastDebugArg + cmd, arg = m.Debugger.lastCmd, m.Debugger.lastArg } else if cmd[0] == '#' { return nil } @@ -169,7 +182,7 @@ func debugCmd(m *Machine) error { if !ok { return errors.New("command not available: " + cmd) } - m.lastDebugCmd, m.lastDebugArg = cmd, arg + m.Debugger.lastCmd, m.Debugger.lastArg = cmd, arg return c.debugFunc(m, arg) } @@ -181,21 +194,21 @@ func debugCmd(m *Machine) error { // not affecting the target program's. // An error during connection setting will result in program panic. func initDebugIO(m *Machine) { - if m.DebugAddr != "" { - l, err := net.Listen("tcp", m.DebugAddr) + if m.Debugger.addr != "" { + l, err := net.Listen("tcp", m.Debugger.addr) if err != nil { panic(err) } - print("Waiting for debugger client to connect at ", m.DebugAddr) + print("Waiting for debugger client to connect at ", m.Debugger.addr) conn, err := l.Accept() if err != nil { panic(err) } println(" connected!") - m.DebugIn = conn - m.DebugOut = conn + m.Debugger.in = conn + m.Debugger.out = conn } - m.DebugScanner = bufio.NewScanner(m.DebugIn) + m.Debugger.scanner = bufio.NewScanner(m.Debugger.in) } // debugUpdateLocation computes the source code location for the current VM state. @@ -208,10 +221,10 @@ func debugUpdateLocation(m *Machine) { loc.File, _ = filepath.Abs(loc.File) } - if m.DebugLoc.PkgPath == "" || - loc.PkgPath != "" && loc.PkgPath != m.DebugLoc.PkgPath || - loc.File != "" && loc.File != m.DebugLoc.File { - m.DebugLoc = loc + if m.Debugger.loc.PkgPath == "" || + loc.PkgPath != "" && loc.PkgPath != m.Debugger.loc.PkgPath || + loc.File != "" && loc.File != m.Debugger.loc.File { + m.Debugger.loc = loc } // The location computed from above points to the block start. Examine @@ -221,7 +234,7 @@ func debugUpdateLocation(m *Machine) { for i := nx - 1; i >= 0; i-- { expr := m.Exprs[i] if l := expr.GetLine(); l > 0 { - m.DebugLoc.Line = l + m.Debugger.loc.Line = l return } } @@ -229,7 +242,7 @@ func debugUpdateLocation(m *Machine) { if len(m.Stmts) > 0 { if stmt := m.PeekStmt1(); stmt != nil { if l := stmt.GetLine(); l > 0 { - m.DebugLoc.Line = l + m.Debugger.loc.Line = l return } } @@ -237,29 +250,31 @@ func debugUpdateLocation(m *Machine) { } // --------------------------------------- -const breakUsage = `break|b [locspec]` -const breakShort = `Set a breakpoint.` -const breakLong = ` +const ( + breakUsage = `break|b [locspec]` + breakShort = `Set a breakpoint.` + breakLong = ` The syntax accepted for locspec is: - : specifies the line in filename. Filename can be relative. - specifies the line in the current source file. - + specifies the line offset lines after the current one. - - specifies the line offset lines before the current one. ` +) func debugBreak(m *Machine, arg string) error { - loc, err := arg2loc(m, arg) + loc, err := parseLocSpec(m, arg) if err != nil { return err } m.breakpoints = append(m.breakpoints, loc) - fmt.Fprintf(m.DebugOut, "Breakpoint %d at %s %s:%d\n", len(m.breakpoints)-1, loc.PkgPath, loc.File, loc.Line) + fmt.Fprintf(m.Debugger.out, "Breakpoint %d at %s %s:%d\n", len(m.breakpoints)-1, loc.PkgPath, loc.File, loc.Line) return nil } -func arg2loc(m *Machine, arg string) (loc Location, err error) { +func parseLocSpec(m *Machine, arg string) (loc Location, err error) { var line int - loc = m.DebugLoc + loc = m.Debugger.loc if strings.Contains(arg, ":") { // Location is specified by filename:line. @@ -293,19 +308,23 @@ func arg2loc(m *Machine, arg string) (loc Location, err error) { } // --------------------------------------- -const breakpointsUsage = `breakpoints|bp` -const breakpointsShort = `Print out info for active breakpoints.` +const ( + breakpointsUsage = `breakpoints|bp` + breakpointsShort = `Print out info for active breakpoints.` +) func debugBreakpoints(m *Machine, arg string) error { for i, b := range m.breakpoints { - fmt.Fprintf(m.DebugOut, "Breakpoint %d at %s %s:%d\n", i, b.PkgPath, b.File, b.Line) + fmt.Fprintf(m.Debugger.out, "Breakpoint %d at %s %s:%d\n", i, b.PkgPath, b.File, b.Line) } return nil } // --------------------------------------- -const clearUsage = `clear [id]` -const clearShort = `Delete breakpoint (all if no id).` +const ( + clearUsage = `clear [id]` + clearShort = `Delete breakpoint (all if no id).` +) func debugClear(m *Machine, arg string) error { if arg != "" { @@ -321,37 +340,47 @@ func debugClear(m *Machine, arg string) error { } // --------------------------------------- -const continueUsage = `continue|c` -const continueShort = `Run until breakpoint or program termination.` +const ( + continueUsage = `continue|c` + continueShort = `Run until breakpoint or program termination.` +) -const stepUsage = `step|s` -const stepShort = `Single step through program.` +const ( + stepUsage = `step|s` + stepShort = `Single step through program.` +) -const stepiUsage = `stepi|si` -const stepiShort = `Single step a single VM instruction.` +const ( + stepiUsage = `stepi|si` + stepiShort = `Single step a single VM instruction.` +) func debugContinue(m *Machine, arg string) error { - m.DebugState = DebugAtRun - m.debugFrameLevel = 0 + m.state = DebugAtRun + m.frameLevel = 0 return nil } // --------------------------------------- -const detachUsage = `detach` -const detachShort = `Close debugger and resume program.` +const ( + detachUsage = `detach` + detachShort = `Close debugger and resume program.` +) func debugDetach(m *Machine, arg string) error { - m.DebugEnabled = false - m.DebugState = DebugAtRun - if i, ok := m.DebugIn.(io.Closer); ok { + m.enabled = false + m.state = DebugAtRun + if i, ok := m.Debugger.in.(io.Closer); ok { i.Close() } return nil } // --------------------------------------- -const downUsage = `down [n]` -const downShort = `Move the current frame down by n (default 1).` +const ( + downUsage = `down [n]` + downShort = `Move the current frame down by n (default 1).` +) func debugDown(m *Machine, arg string) (err error) { n := 1 @@ -360,22 +389,26 @@ func debugDown(m *Machine, arg string) (err error) { return err } } - if level := m.debugFrameLevel - n; level >= 0 && level < len(m.debugCall) { - m.debugFrameLevel = level + if level := m.frameLevel - n; level >= 0 && level < len(m.Debugger.call) { + m.frameLevel = level } debugList(m, "") return nil } // --------------------------------------- -const exitUsage = `exit|quit|q` -const exitShort = `Exit the debugger and program.` +const ( + exitUsage = `exit|quit|q` + exitShort = `Exit the debugger and program.` +) -func debugExit(m *Machine, arg string) error { m.DebugState = DebugAtExit; return nil } +func debugExit(m *Machine, arg string) error { m.state = DebugAtExit; return nil } // --------------------------------------- -const helpUsage = `help|h [command]` -const helpShort = `Print the help message.` +const ( + helpUsage = `help|h [command]` + helpShort = `Print the help message.` +) func debugHelp(m *Machine, arg string) error { c, ok := debugCmds[arg] @@ -387,7 +420,7 @@ func debugHelp(m *Machine, arg string) error { if c.long != "" { t += "\n" + c.long } - fmt.Fprintln(m.DebugOut, t) + fmt.Fprintln(m.Debugger.out, t) return nil } t := "The following commands are available:\n\n" @@ -396,34 +429,36 @@ func debugHelp(m *Machine, arg string) error { t += fmt.Sprintf("%-25s %s\n", c.usage, c.short) } t += "\nType help followed by a command for full documentation." - fmt.Fprintln(m.DebugOut, t) + fmt.Fprintln(m.Debugger.out, t) return nil } // --------------------------------------- -const listUsage = `list|l [locspec]` -const listShort = `Show source code.` -const listLong = ` +const ( + listUsage = `list|l [locspec]` + listShort = `Show source code.` + listLong = ` See 'help break' for locspec syntax. If locspec is empty, list shows the source code around the current line. ` +) func debugList(m *Machine, arg string) (err error) { - loc := m.DebugLoc + loc := m.Debugger.loc hideCursor := false if arg == "" { debugLineInfo(m) - if m.lastDebugCmd == "up" || m.lastDebugCmd == "down" { - loc = debugFrameLoc(m, m.debugFrameLevel) - fmt.Fprintf(m.DebugOut, "Frame %d: %s:%d\n", m.debugFrameLevel, loc.File, loc.Line) + if m.Debugger.lastCmd == "up" || m.Debugger.lastCmd == "down" { + loc = debugFrameLoc(m, m.frameLevel) + fmt.Fprintf(m.Debugger.out, "Frame %d: %s:%d\n", m.frameLevel, loc.File, loc.Line) } } else { - if loc, err = arg2loc(m, arg); err != nil { + if loc, err = parseLocSpec(m, arg); err != nil { return err } hideCursor = true - fmt.Fprintf(m.DebugOut, "Showing %s:%d\n", loc.File, loc.Line) + fmt.Fprintf(m.Debugger.out, "Showing %s:%d\n", loc.File, loc.Line) } file, line := loc.File, loc.Line lines, offset, err := sourceLines(file, line) @@ -435,7 +470,7 @@ func debugList(m *Machine, arg string) (err error) { if !hideCursor && file == loc.File && loc.Line == i+offset { cursor = "=>" } - fmt.Fprintf(m.DebugOut, "%2s %4d: %s\n", cursor, i+offset, l) + fmt.Fprintf(m.Debugger.out, "%2s %4d: %s\n", cursor, i+offset, l) } return nil } @@ -448,7 +483,7 @@ func debugLineInfo(m *Machine) { line += "." + string(f.Func.Name) + "()" } } - fmt.Fprintf(m.DebugOut, "> %s %s:%d\n", line, m.DebugLoc.File, m.DebugLoc.Line) + fmt.Fprintf(m.Debugger.out, "> %s %s:%d\n", line, m.Debugger.loc.File, m.Debugger.loc.Line) } const listLength = 10 // number of lines to display @@ -479,8 +514,10 @@ func min(a, b int) int { } // --------------------------------------- -const printUsage = `print|p ` -const printShort = `Print a variable or expression.` +const ( + printUsage = `print|p ` + printShort = `Print a variable or expression.` +) func debugPrint(m *Machine, arg string) (err error) { if arg == "" { @@ -495,12 +532,12 @@ func debugPrint(m *Machine, arg string) (err error) { if err != nil { return err } - fmt.Println(m.DebugOut, tv) + fmt.Fprintln(m.Debugger.out, tv) return nil } // debugEvalExpr evaluates a Go expression in the context of the VM and returns -// the corresponding value, or an error. +// the corresponding typed value, or an error. // The supported expression syntax is a small subset of Go expressions: // basic literals, identifiers, selectors, index expressions, or a combination // of those are supported, but none of function calls, arithmetic, logic or @@ -582,7 +619,7 @@ func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { if m.Frames[i].Func != nil { funBlock = m.Frames[i].Func.Source } - if ncall == m.debugFrameLevel { + if ncall == m.frameLevel { break } if m.Frames[i].Func != nil { @@ -619,7 +656,7 @@ func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { sblocks = append(sblocks, m.Blocks[0]) // Add global block } - // Search value in current frame level blocks, or main. + // Search value in current frame level blocks, or main scope. for _, b := range sblocks { for i, s := range b.Source.GetBlockNames() { if string(s) == name { @@ -631,8 +668,10 @@ func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { } // --------------------------------------- -const stackUsage = `stack|bt` -const stackShort = `Print stack trace.` +const ( + stackUsage = `stack|bt` + stackShort = `Print stack trace.` +) func debugStack(m *Machine, arg string) error { i := 0 @@ -648,7 +687,7 @@ func debugStack(m *Machine, arg string) error { } else { fname = fmt.Sprintf("%v.%v", ff.PkgPath, ff.Name) } - fmt.Fprintf(m.DebugOut, "%d\tin %s\n\tat %s:%d\n", i, fname, loc.File, loc.Line) + fmt.Fprintf(m.Debugger.out, "%d\tin %s\n\tat %s:%d\n", i, fname, loc.File, loc.Line) i++ } return nil @@ -669,15 +708,17 @@ func debugFrameFunc(m *Machine, n int) *FuncValue { } func debugFrameLoc(m *Machine, n int) Location { - if n == 0 || len(m.debugCall) == 0 { - return m.DebugLoc + if n == 0 || len(m.Debugger.call) == 0 { + return m.Debugger.loc } - return m.debugCall[len(m.debugCall)-n] + return m.Debugger.call[len(m.Debugger.call)-n] } // --------------------------------------- -const upUsage = `up [n]` -const upShort = `Move the current frame up by n (default 1).` +const ( + upUsage = `up [n]` + upShort = `Move the current frame up by n (default 1).` +) func debugUp(m *Machine, arg string) (err error) { n := 1 @@ -686,8 +727,8 @@ func debugUp(m *Machine, arg string) (err error) { return err } } - if level := m.debugFrameLevel + n; level >= 0 && level < len(m.debugCall) { - m.debugFrameLevel = level + if level := m.frameLevel + n; level >= 0 && level < len(m.Debugger.call) { + m.frameLevel = level } debugList(m, "") return nil diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 9d55c77617a..8caf187767b 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -69,7 +69,8 @@ type MachineOptions struct { CheckTypes bool // not yet used ReadOnly bool Debug bool - DebugAddr string // debugger io stream address (stdin/stdout if empty) + DebugAddr string // debugger io stream address (stdin/stdout if empty) + Input io.Reader // used for default debugger input only Output io.Writer Store Store Context interface{} @@ -129,8 +130,10 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { mm.Output = output mm.Store = store mm.Context = context - mm.DebugEnabled = opts.Debug - mm.DebugAddr = opts.DebugAddr + mm.Debugger.enabled = opts.Debug + mm.Debugger.addr = opts.DebugAddr + mm.Debugger.in = opts.Input + mm.Debugger.out = output if pv != nil { mm.SetActivePackage(pv) @@ -1026,7 +1029,7 @@ const ( func (m *Machine) Run() { for { - if m.DebugEnabled { + if m.Debugger.enabled { m.Debug() } op := m.PopOp() From d4cda36eded56b239d877781f6e94100f8408861 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 4 Apr 2024 12:45:04 +0200 Subject: [PATCH 20/37] fix typo --- gnovm/pkg/gnolang/machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 36c540698b8..8eb087f596c 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -96,7 +96,7 @@ type MachineOptions struct { DebugAddr string // debugger io stream address (stdin/stdout if empty) Input io.Reader // used for default debugger input only Output io.Writer // default os.Stdout - Store Store. // default NewStore(Alloc, nil, nil) + Store Store // default NewStore(Alloc, nil, nil) Context interface{} Alloc *Allocator // or see MaxAllocBytes. MaxAllocBytes int64 // or 0 for no limit. From c7e14a67ddf174c5774c080e84e9f6b9e8202eb0 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 4 Apr 2024 13:23:12 +0200 Subject: [PATCH 21/37] fix thelper lint --- gnovm/cmd/gno/debug_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gnovm/cmd/gno/debug_test.go b/gnovm/cmd/gno/debug_test.go index 066cac73222..6d2eeda362d 100644 --- a/gnovm/cmd/gno/debug_test.go +++ b/gnovm/cmd/gno/debug_test.go @@ -14,6 +14,7 @@ type dtest struct{ in, out string } const debugTarget = "../../tests/integ/debugger/sample.gno" func runDebugTest(t *testing.T, tests []dtest) { + t.Helper() args := []string{"run", "-debug", debugTarget} for _, test := range tests { From 860d8bcdb890aaac42ab941f717e99af2316d6a1 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 4 Apr 2024 13:59:24 +0200 Subject: [PATCH 22/37] simplify after bump to go1.21 --- gnovm/pkg/gnolang/debugger.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index c5e7a64418e..8a15bb1c9fc 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -78,7 +78,7 @@ func init() { "print": {debugPrint, printUsage, printShort, ""}, "stack": {debugStack, stackUsage, stackShort, ""}, // NOTE: the difference between continue, step and stepi is handled within - // the main Debug() loop.:w + // the main Debug() loop. "step": {debugContinue, stepUsage, stepShort, ""}, "stepi": {debugContinue, stepiUsage, stepiShort, ""}, "up": {debugUp, upUsage, upShort, ""}, @@ -89,7 +89,7 @@ func init() { for name := range debugCmds { debugCmdNames = append(debugCmdNames, name) } - sort.SliceStable(debugCmdNames, func(i, j int) bool { return debugCmdNames[i] < debugCmdNames[j] }) + sort.Strings(debugCmdNames) // Set command aliases. debugCmds["b"] = debugCmds["break"] @@ -499,20 +499,6 @@ func sourceLines(name string, n int) ([]string, int, error) { return lines[start:end], start + 1, nil } -func max(a, b int) int { - if a > b { - return a - } - return b -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - // --------------------------------------- const ( printUsage = `print|p ` From 7fe0fec05e83e835204a512b856971acaa18742e Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 5 Apr 2024 17:21:24 +0200 Subject: [PATCH 23/37] move debugger_test.go in pkg/gnolang to improve test coverage --- .../gnolang/debugger_test.go} | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) rename gnovm/{cmd/gno/debug_test.go => pkg/gnolang/debugger_test.go} (53%) diff --git a/gnovm/cmd/gno/debug_test.go b/gnovm/pkg/gnolang/debugger_test.go similarity index 53% rename from gnovm/cmd/gno/debug_test.go rename to gnovm/pkg/gnolang/debugger_test.go index 6d2eeda362d..d1180237d3f 100644 --- a/gnovm/cmd/gno/debug_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -1,37 +1,60 @@ -package main +package gnolang_test import ( "bytes" - "context" + "io" "strings" "testing" - "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/tests" ) type dtest struct{ in, out string } const debugTarget = "../../tests/integ/debugger/sample.gno" +type writeNopCloser struct{ io.Writer } + +func (writeNopCloser) Close() error { return nil } + +func eval(in, file string) (string, string) { + out := bytes.NewBufferString("") + err := bytes.NewBufferString("") + stdin := bytes.NewBufferString(in) + stdout := writeNopCloser{out} + stderr := writeNopCloser{err} + + testStore := tests.TestStore(gnoenv.RootDir(), "", stdin, stdout, stderr, tests.ImportModeStdlibsPreferred) + + f := gnolang.MustReadFile(file) + + m := gnolang.NewMachineWithOptions(gnolang.MachineOptions{ + PkgPath: string(f.PkgName), + Input: stdin, + Output: stdout, + Store: testStore, + Debug: true, + }) + + defer m.Release() + + m.RunFiles(f) + ex, _ := gnolang.ParseExpr("main()") + m.Eval(ex) + return out.String(), err.String() +} + func runDebugTest(t *testing.T, tests []dtest) { t.Helper() - args := []string{"run", "-debug", debugTarget} for _, test := range tests { - test := test t.Run("", func(t *testing.T) { - out := bytes.NewBufferString("") - err := bytes.NewBufferString("") - io := commands.NewTestIO() - io.SetIn(bytes.NewBufferString(test.in)) - io.SetOut(commands.WriteNopCloser(out)) - io.SetErr(commands.WriteNopCloser(err)) - if err := newGnocliCmd(io).ParseAndRun(context.Background(), args); err != nil { - t.Fatal(err) - } + out, err := eval(test.in, debugTarget) t.Log("in:", test.in, "out:", out, "err:", err) - if !strings.Contains(out.String(), test.out) { - t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out.String()) + if !strings.Contains(out, test.out) { + t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out) } }) } From a8edb0a316f4b25ef9e7f44e23c4ddc7c70923ff Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Sat, 6 Apr 2024 00:22:33 +0200 Subject: [PATCH 24/37] improve test coverage --- gnovm/cmd/gno/run_test.go | 8 ++++ gnovm/pkg/gnolang/debugger_test.go | 73 ++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index 575798a78dc..2ac300d2a82 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -67,6 +67,14 @@ func TestRunApp(t *testing.T) { args: []string{"run", "../../tests/integ/undefined-variable-test/undefined_variables_test.gno"}, recoverShouldContain: "--- preprocess stack ---", // should contain preprocess debug stack trace }, + { + args: []string{"run", "-debug", "../../tests/integ/debugger/sample.gno"}, + stdoutShouldContain: "Welcome to the Gnovm debugger", + }, + { + args: []string{"run", "-debug-addr", "invalidhost:17538", "../../tests/integ/debugger/sample.gno"}, + recoverShouldContain: "listen tcp: lookup invalidhost: no such host", + }, // TODO: a test file // TODO: args // TODO: nativeLibs VS stdlibs diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index d1180237d3f..eb32e03b3a9 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -1,10 +1,14 @@ package gnolang_test import ( + "bufio" "bytes" + "fmt" "io" + "net" "strings" "testing" + "time" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnolang" @@ -19,7 +23,7 @@ type writeNopCloser struct{ io.Writer } func (writeNopCloser) Close() error { return nil } -func eval(in, file string) (string, string) { +func eval(debugAddr, in, file string) (string, string) { out := bytes.NewBufferString("") err := bytes.NewBufferString("") stdin := bytes.NewBufferString(in) @@ -31,11 +35,12 @@ func eval(in, file string) (string, string) { f := gnolang.MustReadFile(file) m := gnolang.NewMachineWithOptions(gnolang.MachineOptions{ - PkgPath: string(f.PkgName), - Input: stdin, - Output: stdout, - Store: testStore, - Debug: true, + PkgPath: string(f.PkgName), + Input: stdin, + Output: stdout, + Store: testStore, + Debug: true, + DebugAddr: debugAddr, }) defer m.Release() @@ -51,7 +56,7 @@ func runDebugTest(t *testing.T, tests []dtest) { for _, test := range tests { t.Run("", func(t *testing.T) { - out, err := eval(test.in, debugTarget) + out, err := eval("", test.in, debugTarget) t.Log("in:", test.in, "out:", out, "err:", err) if !strings.Contains(out, test.out) { t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out) @@ -74,7 +79,59 @@ func TestDebug(t *testing.T) { {in: cont, out: "=> 7: println(name, i)"}, {in: cont + "stack\n", out: "2 in main.main"}, {in: cont + "up\n", out: "=> 11: f(s, n)"}, + {in: cont + "up\nup\ndown\n", out: "=> 11: f(s, n)"}, {in: cont + "print name\n", out: `("hello" string)`}, - {in: cont + "p i\n", out: `(3 int)`}, + {in: cont + "p i\n", out: "(3 int)"}, + {in: cont + "bp\n", out: "Breakpoint 0 at main "}, + {in: "p 3\n", out: "(3 int)"}, + {in: "p 'a'\n", out: "(39 int32)"}, + {in: "p \"xxxx\"\n", out: `("\"xxxx\"" string)`}, + {in: "si\n", out: "sample.gno:4"}, + {in: "s\ns\n", out: "=> 17: num := 5"}, + {in: "s\n\n", out: "=> 17: num := 5"}, + {in: "foo", out: "command not available: foo"}, + {in: "\n\n", out: "dbg> "}, + {in: "#\n", out: "dbg> "}, + {in: "p foo", out: "Command failed: could not find symbol value for foo"}, + {in: "b +7\nc\n", out: "=> 11:"}, + {in: brk + "clear 0\n", out: "dbg> "}, + {in: brk + "clear -1\n", out: "Command failed: invalid breakpoint id: -1"}, + {in: brk + "clear\n", out: "dbg> "}, + {in: "p\n", out: "Command failed: missing argument"}, + {in: "p 1+2\n", out: "Command failed: expression not supported"}, + {in: "p 1.2\n", out: "Command failed: invalid basic literal value: 1.2"}, + {in: "p 31212324222123123232123123123123123123123123123123\n", out: "value out of range"}, + {in: "p 3)\n", out: "Command failed:"}, }) } + +const debugAddress = "localhost:17358" + +func TestRemoteDebug(t *testing.T) { + var ( + conn net.Conn + err error + retry int + ) + + go eval(debugAddress, "", debugTarget) + + for retry = 100; retry > 0; retry-- { + conn, err = net.Dial("tcp", debugAddress) + if err == nil { + break + } + time.Sleep(10 * time.Millisecond) + } + if retry == 0 { + t.Error(err) + } + defer conn.Close() + + fmt.Fprintf(conn, "d\n") + resp, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + t.Error(err) + } + t.Log("resp:", resp) +} From 79096bac119d36ad4b3e5292bdd40d6d99fd9d8c Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Sat, 6 Apr 2024 12:14:10 +0200 Subject: [PATCH 25/37] fix test --- gnovm/cmd/gno/run_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index 2ac300d2a82..77b0d72277e 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -73,7 +73,7 @@ func TestRunApp(t *testing.T) { }, { args: []string{"run", "-debug-addr", "invalidhost:17538", "../../tests/integ/debugger/sample.gno"}, - recoverShouldContain: "listen tcp: lookup invalidhost: no such host", + recoverShouldContain: "listen tcp: lookup invalidhost", }, // TODO: a test file // TODO: args From e4ebcc7e8528676eba4ae1e39d9774f4d5c2b388 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 11 Apr 2024 17:04:32 +0200 Subject: [PATCH 26/37] fix: add boundary check for list --- gnovm/pkg/gnolang/debugger.go | 3 +++ gnovm/pkg/gnolang/debugger_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 8a15bb1c9fc..ba05d98c478 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -496,6 +496,9 @@ func sourceLines(name string, n int) ([]string, int, error) { lines := strings.Split(string(buf), "\n") start := max(1, n-listLength/2) - 1 end := min(start+listLength, len(lines)) + if start >= end { + start = max(1, end-listLength) + } return lines[start:end], start + 1, nil } diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index eb32e03b3a9..87417b6ed16 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -75,6 +75,7 @@ func TestDebug(t *testing.T) { {in: "h\n", out: "The following commands are available"}, {in: "help h\n", out: "Print the help message."}, {in: "list " + debugTarget + ":1\n", out: "1: // This is a sample target"}, + {in: "l 40\n", out: "23: }"}, {in: brk, out: "Breakpoint 0 at main "}, {in: cont, out: "=> 7: println(name, i)"}, {in: cont + "stack\n", out: "2 in main.main"}, From 445c56505dfebddef9af1e4acd5d69b31d23c87a Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Wed, 24 Apr 2024 11:09:07 +0200 Subject: [PATCH 27/37] fix printing literal string and rune. Allow space in debug args. --- gnovm/pkg/gnolang/debugger.go | 84 +++++++++++++++++++++--------- gnovm/pkg/gnolang/debugger_test.go | 5 +- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index ba05d98c478..900a018e63f 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "go/ast" - "go/parser" "go/token" "io" "net" @@ -14,6 +13,7 @@ import ( "sort" "strconv" "strings" + "unicode" ) // DebugState is the state of the machine debugger, defined by a finite state @@ -168,9 +168,14 @@ func debugCmd(m *Machine) error { if !m.Debugger.scanner.Scan() { return debugDetach(m, arg) // Clean close of debugger, the target program resumes. } - line := m.Debugger.scanner.Text() - n, _ := fmt.Sscan(line, &cmd, &arg) - if n == 0 { + line := trimLeftSpace(m.Debugger.scanner.Text()) + if i := indexSpace(line); i >= 0 { + cmd = line[:i] + arg = trimLeftSpace(line[i:]) + } else { + cmd = line + } + if cmd == "" { if m.Debugger.lastCmd == "" { return nil } @@ -186,6 +191,9 @@ func debugCmd(m *Machine) error { return c.debugFunc(m, arg) } +func trimLeftSpace(s string) string { return strings.TrimLeftFunc(s, unicode.IsSpace) } +func indexSpace(s string) int { return strings.IndexFunc(s, unicode.IsSpace) } + // initDebugIO initializes the debugger standard input and output streams. // If no debug address was specified at program start, the debugger will inherit its // standard input and output from the process, which will be shared with the target program. @@ -268,10 +276,15 @@ func debugBreak(m *Machine, arg string) error { return err } m.breakpoints = append(m.breakpoints, loc) - fmt.Fprintf(m.Debugger.out, "Breakpoint %d at %s %s:%d\n", len(m.breakpoints)-1, loc.PkgPath, loc.File, loc.Line) + printBreakpoint(m, len(m.breakpoints)-1) return nil } +func printBreakpoint(m *Machine, i int) { + b := m.Debugger.breakpoints[i] + fmt.Fprintf(m.Debugger.out, "Breakpoint %d at %s %s:%d\n", i, b.PkgPath, b.File, b.Line) +} + func parseLocSpec(m *Machine, arg string) (loc Location, err error) { var line int loc = m.Debugger.loc @@ -314,8 +327,8 @@ const ( ) func debugBreakpoints(m *Machine, arg string) error { - for i, b := range m.breakpoints { - fmt.Fprintf(m.Debugger.out, "Breakpoint %d at %s %s:%d\n", i, b.PkgPath, b.File, b.Line) + for i := range m.breakpoints { + printBreakpoint(m, i) } return nil } @@ -356,8 +369,8 @@ const ( ) func debugContinue(m *Machine, arg string) error { - m.state = DebugAtRun - m.frameLevel = 0 + m.Debugger.tate = DebugAtRun + m.Debugger.rameLevel = 0 return nil } @@ -389,8 +402,8 @@ func debugDown(m *Machine, arg string) (err error) { return err } } - if level := m.frameLevel - n; level >= 0 && level < len(m.Debugger.call) { - m.frameLevel = level + if level := m.Debugger.rameLevel - n; level >= 0 && level < len(m.Debugger.call) { + m.Debugger.rameLevel = level } debugList(m, "") return nil @@ -450,8 +463,8 @@ func debugList(m *Machine, arg string) (err error) { if arg == "" { debugLineInfo(m) if m.Debugger.lastCmd == "up" || m.Debugger.lastCmd == "down" { - loc = debugFrameLoc(m, m.frameLevel) - fmt.Fprintf(m.Debugger.out, "Frame %d: %s:%d\n", m.frameLevel, loc.File, loc.Line) + loc = debugFrameLoc(m, m.Debugger.frameLevel) + fmt.Fprintf(m.Debugger.out, "Frame %d: %s:%d\n", m.Debugger.frameLevel, loc.File, loc.Line) } } else { if loc, err = parseLocSpec(m, arg); err != nil { @@ -512,16 +525,29 @@ func debugPrint(m *Machine, arg string) (err error) { if arg == "" { return errors.New("missing argument") } - // Use the Go parser to get the AST representation of print argument as a Go expresssion. - ast, err := parser.ParseExpr(arg) + // /* + expr, err := ParseExpr(arg) if err != nil { return err } - tv, err := debugEvalExpr(m, ast) - if err != nil { - return err - } - fmt.Fprintln(m.Debugger.out, tv) + m.Debugger.enabled = false + defer func() { m.Debugger.enabled = true }() + + res := m.Eval(expr) + fmt.Fprintln(m.Debugger.out, res[0]) + // */ + /* + // Use the Go parser to get the AST representation of print argument as a Go expresssion. + ast, err := parser.ParseExpr(arg) + if err != nil { + return err + } + tv, err := debugEvalExpr(m, ast) + if err != nil { + return err + } + fmt.Fprintln(m.Debugger.out, tv) + */ return nil } @@ -543,9 +569,17 @@ func debugEvalExpr(m *Machine, node ast.Node) (tv TypedValue, err error) { } return typedInt(int(i)), nil case token.CHAR: - return typedRune(([]rune(n.Value))[0]), nil + r, _, _, err := strconv.UnquoteChar(n.Value[1:len(n.Value)-1], 0) + if err != nil { + return tv, err + } + return typedRune(r), nil case token.STRING: - return typedString(n.Value), nil + s, err := strconv.Unquote(n.Value) + if err != nil { + return tv, err + } + return typedString(s), nil } return tv, fmt.Errorf("invalid basic literal value: %s", n.Value) case *ast.Ident: @@ -608,7 +642,7 @@ func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { if m.Frames[i].Func != nil { funBlock = m.Frames[i].Func.Source } - if ncall == m.frameLevel { + if ncall == m.Debugger.frameLevel { break } if m.Frames[i].Func != nil { @@ -716,8 +750,8 @@ func debugUp(m *Machine, arg string) (err error) { return err } } - if level := m.frameLevel + n; level >= 0 && level < len(m.Debugger.call) { - m.frameLevel = level + if level := m.Debugger.frameLevel + n; level >= 0 && level < len(m.Debugger.call) { + m.Debugger.rameLevel = level } debugList(m, "") return nil diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 87417b6ed16..609d2869268 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -85,8 +85,9 @@ func TestDebug(t *testing.T) { {in: cont + "p i\n", out: "(3 int)"}, {in: cont + "bp\n", out: "Breakpoint 0 at main "}, {in: "p 3\n", out: "(3 int)"}, - {in: "p 'a'\n", out: "(39 int32)"}, - {in: "p \"xxxx\"\n", out: `("\"xxxx\"" string)`}, + {in: "p 'a'\n", out: "(97 int32)"}, + {in: "p '界'\n", out: "(30028 int32)"}, + {in: "p \"xxxx\"\n", out: `("xxxx" string)`}, {in: "si\n", out: "sample.gno:4"}, {in: "s\ns\n", out: "=> 17: num := 5"}, {in: "s\n\n", out: "=> 17: num := 5"}, From 26dd6a3d4a0503f37e8f2fd4b24de64d9d56b710 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Wed, 24 Apr 2024 11:21:22 +0200 Subject: [PATCH 28/37] typos --- gnovm/pkg/gnolang/debugger.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 900a018e63f..8cc39dcf742 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -369,8 +369,8 @@ const ( ) func debugContinue(m *Machine, arg string) error { - m.Debugger.tate = DebugAtRun - m.Debugger.rameLevel = 0 + m.Debugger.state = DebugAtRun + m.Debugger.frameLevel = 0 return nil } @@ -402,8 +402,8 @@ func debugDown(m *Machine, arg string) (err error) { return err } } - if level := m.Debugger.rameLevel - n; level >= 0 && level < len(m.Debugger.call) { - m.Debugger.rameLevel = level + if level := m.Debugger.frameLevel - n; level >= 0 && level < len(m.Debugger.call) { + m.Debugger.frameLevel = level } debugList(m, "") return nil @@ -751,7 +751,7 @@ func debugUp(m *Machine, arg string) (err error) { } } if level := m.Debugger.frameLevel + n; level >= 0 && level < len(m.Debugger.call) { - m.Debugger.rameLevel = level + m.Debugger.frameLevel = level } debugList(m, "") return nil From a69a49698265d58e43c3cc5b3d506361bcb7a314 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Wed, 24 Apr 2024 11:23:18 +0200 Subject: [PATCH 29/37] revert alternate debugPrint --- gnovm/pkg/gnolang/debugger.go | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 8cc39dcf742..1dc4937fabf 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "go/ast" + "go/parser" "go/token" "io" "net" @@ -525,29 +526,16 @@ func debugPrint(m *Machine, arg string) (err error) { if arg == "" { return errors.New("missing argument") } - // /* - expr, err := ParseExpr(arg) + // Use the Go parser to get the AST representation of print argument as a Go expresssion. + ast, err := parser.ParseExpr(arg) if err != nil { return err } - m.Debugger.enabled = false - defer func() { m.Debugger.enabled = true }() - - res := m.Eval(expr) - fmt.Fprintln(m.Debugger.out, res[0]) - // */ - /* - // Use the Go parser to get the AST representation of print argument as a Go expresssion. - ast, err := parser.ParseExpr(arg) - if err != nil { - return err - } - tv, err := debugEvalExpr(m, ast) - if err != nil { - return err - } - fmt.Fprintln(m.Debugger.out, tv) - */ + tv, err := debugEvalExpr(m, ast) + if err != nil { + return err + } + fmt.Fprintln(m.Debugger.out, tv) return nil } From 3b19e1ab68e679e38e1cc00cf2f1f6b59498f8f6 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 25 Apr 2024 16:29:13 +0200 Subject: [PATCH 30/37] fix list and break commands when source file is not set Fix also list for sources comming from memory packages. --- gnovm/pkg/gnolang/debugger.go | 58 ++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 1dc4937fabf..02779299618 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -126,7 +126,7 @@ loop: m.Debugger.state = DebugAtCmd debugLineInfo(m) case "s", "step": - if m.Debugger.loc != m.Debugger.prevLoc { + if m.Debugger.loc != m.Debugger.prevLoc && m.Debugger.loc.File != "" { m.Debugger.state = DebugAtCmd m.Debugger.prevLoc = m.Debugger.loc debugList(m, "") @@ -225,9 +225,8 @@ func initDebugIO(m *Machine) { func debugUpdateLocation(m *Machine) { loc := m.LastBlock().Source.GetLocation() - // File must have an unambiguous absolute path. - if loc.File != "" && !filepath.IsAbs(loc.File) { - loc.File, _ = filepath.Abs(loc.File) + if strings.Contains(loc.File, "/") { + loc.PkgPath = "" // Path is already included in file. } if m.Debugger.loc.PkgPath == "" || @@ -305,6 +304,10 @@ func parseLocSpec(m *Machine, arg string) (loc Location, err error) { loc.Line = line return loc, nil } + // Location is in the current file. + if loc.File == "" { + return loc, errors.New("unknown source file") + } if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") { // Location is specified as a line offset from the current line. if line, err = strconv.Atoi(arg); err != nil { @@ -465,23 +468,26 @@ func debugList(m *Machine, arg string) (err error) { debugLineInfo(m) if m.Debugger.lastCmd == "up" || m.Debugger.lastCmd == "down" { loc = debugFrameLoc(m, m.Debugger.frameLevel) - fmt.Fprintf(m.Debugger.out, "Frame %d: %s:%d\n", m.Debugger.frameLevel, loc.File, loc.Line) + fmt.Fprintf(m.Debugger.out, "Frame %d: %s\n", m.Debugger.frameLevel, loc) } } else { if loc, err = parseLocSpec(m, arg); err != nil { return err } hideCursor = true - fmt.Fprintf(m.Debugger.out, "Showing %s:%d\n", loc.File, loc.Line) + fmt.Fprintf(m.Debugger.out, "Showing %s\n", loc) + } + if loc.File == "" && (m.Debugger.lastCmd == "list" || m.Debugger.lastCmd == "l") { + return errors.New("unknown source file") } - file, line := loc.File, loc.Line - lines, offset, err := sourceLines(file, line) + src, err := fileContent(m.Store, loc.PkgPath, loc.File) if err != nil { return err } + lines, offset := linesAround(src, loc.Line, 10) for i, l := range lines { cursor := "" - if !hideCursor && file == loc.File && loc.Line == i+offset { + if !hideCursor && loc.Line == i+offset { cursor = "=>" } fmt.Fprintf(m.Debugger.out, "%2s %4d: %s\n", cursor, i+offset, l) @@ -490,6 +496,9 @@ func debugList(m *Machine, arg string) (err error) { } func debugLineInfo(m *Machine) { + if m.Debugger.loc.File == "" { + return + } line := string(m.Package.PkgName) if len(m.Frames) > 0 { f := m.Frames[len(m.Frames)-1] @@ -497,23 +506,30 @@ func debugLineInfo(m *Machine) { line += "." + string(f.Func.Name) + "()" } } - fmt.Fprintf(m.Debugger.out, "> %s %s:%d\n", line, m.Debugger.loc.File, m.Debugger.loc.Line) + fmt.Fprintf(m.Debugger.out, "> %s %s\n", line, m.Debugger.loc) } -const listLength = 10 // number of lines to display +func isMemPackage(st Store, pkgPath string) bool { + ds, ok := st.(*defaultStore) + return ok && ds.iavlStore.Has([]byte(backendPackagePathKey(pkgPath))) +} -func sourceLines(name string, n int) ([]string, int, error) { - buf, err := os.ReadFile(name) - if err != nil { - return nil, 1, err +func fileContent(st Store, pkgPath, name string) (string, error) { + if isMemPackage(st, pkgPath) { + return st.GetMemFile(pkgPath, name).Body, nil } - lines := strings.Split(string(buf), "\n") - start := max(1, n-listLength/2) - 1 - end := min(start+listLength, len(lines)) + buf, err := os.ReadFile(name) + return string(buf), err +} + +func linesAround(src string, index, n int) ([]string, int) { + lines := strings.Split(src, "\n") + start := max(1, index-n/2) - 1 + end := min(start+n, len(lines)) if start >= end { - start = max(1, end-listLength) + start = max(1, end-n) } - return lines[start:end], start + 1, nil + return lines[start:end], start + 1 } // --------------------------------------- @@ -698,7 +714,7 @@ func debugStack(m *Machine, arg string) error { } else { fname = fmt.Sprintf("%v.%v", ff.PkgPath, ff.Name) } - fmt.Fprintf(m.Debugger.out, "%d\tin %s\n\tat %s:%d\n", i, fname, loc.File, loc.Line) + fmt.Fprintf(m.Debugger.out, "%d\tin %s\n\tat %s\n", i, fname, loc) i++ } return nil From e573bb47f6a9578bbad563fcb87a09f97186ad5e Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 25 Apr 2024 17:06:35 +0200 Subject: [PATCH 31/37] fix previous --- gnovm/pkg/gnolang/debugger.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 02779299618..c18c2bbc1c9 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -225,10 +225,6 @@ func initDebugIO(m *Machine) { func debugUpdateLocation(m *Machine) { loc := m.LastBlock().Source.GetLocation() - if strings.Contains(loc.File, "/") { - loc.PkgPath = "" // Path is already included in file. - } - if m.Debugger.loc.PkgPath == "" || loc.PkgPath != "" && loc.PkgPath != m.Debugger.loc.PkgPath || loc.File != "" && loc.File != m.Debugger.loc.File { @@ -282,7 +278,7 @@ func debugBreak(m *Machine, arg string) error { func printBreakpoint(m *Machine, i int) { b := m.Debugger.breakpoints[i] - fmt.Fprintf(m.Debugger.out, "Breakpoint %d at %s %s:%d\n", i, b.PkgPath, b.File, b.Line) + fmt.Fprintf(m.Debugger.out, "Breakpoint %d at %s %s\n", i, b.PkgPath, b) } func parseLocSpec(m *Machine, arg string) (loc Location, err error) { From 98d9b1f6b89ba9f498eec8a031d50020b7be8be7 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Mon, 6 May 2024 17:54:20 +0200 Subject: [PATCH 32/37] fix: don not panic in case of error of remote debugger connection The connection is now initiated of the debugger state machine, so the error can be processed from the start. It also allows slight simplifcations. --- gnovm/cmd/gno/run.go | 18 ++++++++----- gnovm/pkg/gnolang/debugger.go | 43 ++++++++++++------------------ gnovm/pkg/gnolang/debugger_test.go | 12 ++++++--- gnovm/pkg/gnolang/machine.go | 1 - 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index d3d98f88e83..0c5218613a9 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -112,16 +112,22 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { } m := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: string(files[0].PkgName), - Input: stdin, - Output: stdout, - Store: testStore, - Debug: cfg.debug || cfg.debugAddr != "", - DebugAddr: cfg.debugAddr, + PkgPath: string(files[0].PkgName), + Input: stdin, + Output: stdout, + Store: testStore, + Debug: cfg.debug || cfg.debugAddr != "", }) defer m.Release() + // If the debug address is set, the debugger waits for a remote client to connect to it. + if cfg.debugAddr != "" { + if err := m.Debugger.Serve(cfg.debugAddr); err != nil { + return err + } + } + // run files m.RunFiles(files...) runExpr(m, cfg.expr) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index c18c2bbc1c9..9f29e2674f2 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -39,7 +39,6 @@ const ( // Debugger describes a machine debugger. type Debugger struct { enabled bool // when true, machine is in step mode - addr string // optional network address [host]:port for In/Out in io.Reader // debugger input, defaults to Stdin out io.Writer // debugger output, defaults to Stdout scanner *bufio.Scanner // to parse input per line @@ -112,9 +111,9 @@ loop: for { switch m.Debugger.state { case DebugAtInit: - initDebugIO(m) debugUpdateLocation(m) fmt.Fprintln(m.Debugger.out, "Welcome to the Gnovm debugger. Type 'help' for list of commands.") + m.Debugger.scanner = bufio.NewScanner(m.Debugger.in) m.Debugger.state = DebugAtCmd case DebugAtCmd: if err := debugCmd(m); err != nil { @@ -195,29 +194,21 @@ func debugCmd(m *Machine) error { func trimLeftSpace(s string) string { return strings.TrimLeftFunc(s, unicode.IsSpace) } func indexSpace(s string) int { return strings.IndexFunc(s, unicode.IsSpace) } -// initDebugIO initializes the debugger standard input and output streams. -// If no debug address was specified at program start, the debugger will inherit its -// standard input and output from the process, which will be shared with the target program. -// If the debug address was specified, the program will be blocked until -// a client connection is established. The debugger will use this connection and -// not affecting the target program's. -// An error during connection setting will result in program panic. -func initDebugIO(m *Machine) { - if m.Debugger.addr != "" { - l, err := net.Listen("tcp", m.Debugger.addr) - if err != nil { - panic(err) - } - print("Waiting for debugger client to connect at ", m.Debugger.addr) - conn, err := l.Accept() - if err != nil { - panic(err) - } - println(" connected!") - m.Debugger.in = conn - m.Debugger.out = conn +// Serve waits for a remote client to connect to addr and use this connection for debugger IO. +// It returns an error if the connection can not be established, or nil. +func (d *Debugger) Serve(addr string) error { + l, err := net.Listen("tcp", addr) + if err != nil { + return err } - m.Debugger.scanner = bufio.NewScanner(m.Debugger.in) + print("Waiting for debugger client to connect at ", addr) + conn, err := l.Accept() + if err != nil { + return err + } + println(" connected!") + d.in, d.out = conn, conn + return nil } // debugUpdateLocation computes the source code location for the current VM state. @@ -381,8 +372,8 @@ const ( ) func debugDetach(m *Machine, arg string) error { - m.enabled = false - m.state = DebugAtRun + m.Debugger.enabled = false + m.Debugger.state = DebugAtRun if i, ok := m.Debugger.in.(io.Closer); ok { i.Close() } diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 609d2869268..980da9e22f6 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -23,7 +23,7 @@ type writeNopCloser struct{ io.Writer } func (writeNopCloser) Close() error { return nil } -func eval(debugAddr, in, file string) (string, string) { +func eval(debugAddr, in, file string) (string, string, error) { out := bytes.NewBufferString("") err := bytes.NewBufferString("") stdin := bytes.NewBufferString(in) @@ -45,10 +45,16 @@ func eval(debugAddr, in, file string) (string, string) { defer m.Release() + if debugAddr != "" { + if err := m.Debugger.Serve(debugAddr); err != nil { + return "", "", err + } + } + m.RunFiles(f) ex, _ := gnolang.ParseExpr("main()") m.Eval(ex) - return out.String(), err.String() + return out.String(), err.String(), nil } func runDebugTest(t *testing.T, tests []dtest) { @@ -56,7 +62,7 @@ func runDebugTest(t *testing.T, tests []dtest) { for _, test := range tests { t.Run("", func(t *testing.T) { - out, err := eval("", test.in, debugTarget) + out, err, _ := eval("", test.in, debugTarget) t.Log("in:", test.in, "out:", out, "err:", err) if !strings.Contains(out, test.out) { t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out) diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index acf5dc5b25e..8ca9ad5101b 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -165,7 +165,6 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { mm.Context = context mm.GasMeter = vmGasMeter mm.Debugger.enabled = opts.Debug - mm.Debugger.addr = opts.DebugAddr mm.Debugger.in = opts.Input mm.Debugger.out = output From 3fad6b9e7ec9c71c430d5f534455fd208d426bb3 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 10 May 2024 11:47:49 +0200 Subject: [PATCH 33/37] fix: lookup values in if body. Do not panic if debug expr error. Also improve test coverage (now > 96% for debugger.go). --- gnovm/pkg/gnolang/debugger.go | 22 +++++++++++++ gnovm/pkg/gnolang/debugger_test.go | 47 +++++++++++++++++++++++---- gnovm/tests/integ/debugger/sample.gno | 21 +++++++++++- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 9f29e2674f2..1e0030d49e8 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -550,6 +550,12 @@ func debugPrint(m *Machine, arg string) (err error) { // assign operations, type assertions of convertions. // This is sufficient for a debugger to perform 'print (*f).S[x][y]' for example. func debugEvalExpr(m *Machine, node ast.Node) (tv TypedValue, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + switch n := node.(type) { case *ast.BasicLit: switch n.Kind { @@ -672,6 +678,14 @@ func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { // Search value in current frame level blocks, or main scope. for _, b := range sblocks { + switch t := b.Source.(type) { + case *IfStmt: + for i, s := range ifBody(m, t).Source.GetBlockNames() { + if string(s) == name { + return b.Values[i], true + } + } + } for i, s := range b.Source.GetBlockNames() { if string(s) == name { return b.Values[i], true @@ -681,6 +695,14 @@ func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { return tv, false } +// ifBody returns the Then or Else body corresponding to the current location. +func ifBody(m *Machine, ifStmt *IfStmt) IfCaseStmt { + if l := ifStmt.Else.GetLocation().Line; l > 0 && debugFrameLoc(m, m.Debugger.frameLevel).Line > l { + return ifStmt.Else + } + return ifStmt.Then +} + // --------------------------------------- const ( stackUsage = `stack|bt` diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 980da9e22f6..a123c9d118e 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -57,12 +57,12 @@ func eval(debugAddr, in, file string) (string, string, error) { return out.String(), err.String(), nil } -func runDebugTest(t *testing.T, tests []dtest) { +func runDebugTest(t *testing.T, targetPath string, tests []dtest) { t.Helper() for _, test := range tests { t.Run("", func(t *testing.T) { - out, err, _ := eval("", test.in, debugTarget) + out, err, _ := eval("", test.in, targetPath) t.Log("in:", test.in, "out:", out, "err:", err) if !strings.Contains(out, test.out) { t.Errorf("result does not contain \"%s\", got \"%s\"", test.out, out) @@ -74,29 +74,36 @@ func runDebugTest(t *testing.T, tests []dtest) { func TestDebug(t *testing.T) { brk := "break 7\n" cont := brk + "continue\n" + cont2 := "break 21\ncontinue\n" - runDebugTest(t, []dtest{ + runDebugTest(t, debugTarget, []dtest{ {in: "", out: "Welcome to the Gnovm debugger. Type 'help' for list of commands."}, {in: "help\n", out: "The following commands are available"}, {in: "h\n", out: "The following commands are available"}, - {in: "help h\n", out: "Print the help message."}, + {in: "help b\n", out: "Set a breakpoint."}, + {in: "help zzz\n", out: "command not available"}, {in: "list " + debugTarget + ":1\n", out: "1: // This is a sample target"}, - {in: "l 40\n", out: "23: }"}, + {in: "l 55\n", out: "42: }"}, + {in: "l xxx:0\n", out: "xxx: no such file or directory"}, + {in: "l :xxx\n", out: `"xxx": invalid syntax`}, {in: brk, out: "Breakpoint 0 at main "}, + {in: "break :zzz\n", out: `"zzz": invalid syntax`}, + {in: "b +xxx\n", out: `"+xxx": invalid syntax`}, {in: cont, out: "=> 7: println(name, i)"}, {in: cont + "stack\n", out: "2 in main.main"}, {in: cont + "up\n", out: "=> 11: f(s, n)"}, {in: cont + "up\nup\ndown\n", out: "=> 11: f(s, n)"}, {in: cont + "print name\n", out: `("hello" string)`}, {in: cont + "p i\n", out: "(3 int)"}, + {in: cont + "up\np global\n", out: `("test" string)`}, {in: cont + "bp\n", out: "Breakpoint 0 at main "}, {in: "p 3\n", out: "(3 int)"}, {in: "p 'a'\n", out: "(97 int32)"}, {in: "p '界'\n", out: "(30028 int32)"}, {in: "p \"xxxx\"\n", out: `("xxxx" string)`}, {in: "si\n", out: "sample.gno:4"}, - {in: "s\ns\n", out: "=> 17: num := 5"}, - {in: "s\n\n", out: "=> 17: num := 5"}, + {in: "s\ns\n", out: "=> 33: num := 5"}, + {in: "s\n\n", out: "=> 33: num := 5"}, {in: "foo", out: "command not available: foo"}, {in: "\n\n", out: "dbg> "}, {in: "#\n", out: "dbg> "}, @@ -110,6 +117,25 @@ func TestDebug(t *testing.T) { {in: "p 1.2\n", out: "Command failed: invalid basic literal value: 1.2"}, {in: "p 31212324222123123232123123123123123123123123123123\n", out: "value out of range"}, {in: "p 3)\n", out: "Command failed:"}, + {in: "p (3)", out: "(3 int)"}, + {in: cont2 + "bt\n", out: "0 in main.(*main.T).get"}, + {in: cont2 + "p t.A[2]\n", out: "(3 int)"}, + {in: cont2 + "p t.A[k]\n", out: "could not find symbol value for k"}, + {in: cont2 + "p *t\n", out: "(struct{(slice[(1 int),(2 int),(3 int)] []int)} main.T)"}, + {in: cont2 + "p *i\n", out: "Not a pointer value: (1 int)"}, + {in: cont2 + "p *a\n", out: "could not find symbol value for a"}, + {in: cont2 + "p a[1]\n", out: "could not find symbol value for a"}, + {in: cont2 + "p t.B\n", out: "invalid selector: B"}, + {in: "down xxx", out: `"xxx": invalid syntax`}, + {in: "up xxx", out: `"xxx": invalid syntax`}, + {in: "b 37\nc\np b\n", out: "(3 int)"}, + {in: "b 27\nc\np b\n", out: `("!zero" string)`}, + {in: "b 22\nc\np t.A[3]\n", out: "Command failed: slice index out of bounds: 3 (len=3)"}, + }) + + runDebugTest(t, "../../tests/files/a1.gno", []dtest{ + {in: "l\n", out: "unknown source file"}, + {in: "b 5\n", out: "unknown source file"}, }) } @@ -143,3 +169,10 @@ func TestRemoteDebug(t *testing.T) { } t.Log("resp:", resp) } + +func TestRemoteError(t *testing.T) { + _, _, err := eval(":xxx", "", debugTarget) + if !strings.Contains(err.Error(), "listen tcp: lookup tcp/xxx: unknown port") { + t.Error(err) + } +} diff --git a/gnovm/tests/integ/debugger/sample.gno b/gnovm/tests/integ/debugger/sample.gno index 06a689a78a3..ecd980acf05 100644 --- a/gnovm/tests/integ/debugger/sample.gno +++ b/gnovm/tests/integ/debugger/sample.gno @@ -13,11 +13,30 @@ func g(s string, n int) { var global = "test" +type T struct { + A []int +} + +func (t *T) get(i int) int { + r := t.A[i] + if i == 0 { + b := "zero" + println(b) + } else { + b := "!zero" + println(b) + } + return r +} + func main() { num := 5 println("in main") if num > 2 { - g("hello", 3) + b := 3 + g("hello", b) } + t := T{A: []int{1, 2, 3} } + println(t.get(1)) println("bye") } From a6700bff1593b6965015f3261f988093b76270af Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 10 May 2024 18:00:53 +0200 Subject: [PATCH 34/37] fix test --- gnovm/pkg/gnolang/debugger_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index a123c9d118e..285c1be1093 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -172,7 +172,7 @@ func TestRemoteDebug(t *testing.T) { func TestRemoteError(t *testing.T) { _, _, err := eval(":xxx", "", debugTarget) - if !strings.Contains(err.Error(), "listen tcp: lookup tcp/xxx: unknown port") { + if !strings.Contains(err.Error(), "tcp/xxx: unknown port") { t.Error(err) } } From 1d4bc06e3dc32f69add1b7e7517a4f46b098dafb Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Mon, 13 May 2024 15:45:01 +0200 Subject: [PATCH 35/37] fix printing values in a external package --- gnovm/pkg/gnolang/debugger.go | 11 ++++++++++- gnovm/pkg/gnolang/debugger_test.go | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 1e0030d49e8..b2d2653c297 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -601,7 +601,12 @@ func debugEvalExpr(m *Machine, node ast.Node) (tv TypedValue, err error) { if err != nil { return tv, err } - // TODO: handle selector on package. + if pv, ok := x.V.(*PackageValue); ok { + if i, ok := pv.Block.(*Block).Source.GetLocalIndex(Name(n.Sel.Name)); ok { + return pv.Block.(*Block).Values[i], nil + } + return tv, fmt.Errorf("invalid selector: %s", n.Sel.Name) + } tr, _, _, _, _ := findEmbeddedFieldType(x.T.GetPkgPath(), x.T, Name(n.Sel.Name), nil) if len(tr) == 0 { return tv, fmt.Errorf("invalid selector: %s", n.Sel.Name) @@ -692,6 +697,10 @@ func debugLookup(m *Machine, name string) (tv TypedValue, ok bool) { } } } + // Fallback: search a global value. + if v := sblocks[0].Source.GetValueRef(m.Store, Name(name)); v != nil { + return *v, true + } return tv, false } diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 285c1be1093..ca50e243a5c 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -137,6 +137,12 @@ func TestDebug(t *testing.T) { {in: "l\n", out: "unknown source file"}, {in: "b 5\n", out: "unknown source file"}, }) + + runDebugTest(t, "../../tests/integ/debugger/sample2.gno", []dtest{ + {in: "s\np tests\n", out: "(package(tests gno.land/p/demo/tests) package{})"}, + {in: "s\np tests.World\n", out: `("world" string)`}, + {in: "s\np tests.xxx\n", out: "Command failed: invalid selector: xxx"}, + }) } const debugAddress = "localhost:17358" From a7a2602f575d341e330db74155080fc8096af427 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Mon, 13 May 2024 15:54:46 +0200 Subject: [PATCH 36/37] fix: no need to embed the Debugger in Machine. Explicit is ok. --- gnovm/pkg/gnolang/debugger.go | 16 ++++++++-------- gnovm/pkg/gnolang/machine.go | 2 +- gnovm/tests/integ/debugger/sample2.gno | 8 ++++++++ 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 gnovm/tests/integ/debugger/sample2.gno diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index b2d2653c297..2a080da47a3 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -132,7 +132,7 @@ loop: continue loop } default: - for _, b := range m.breakpoints { + for _, b := range m.Debugger.breakpoints { if b == m.Debugger.loc && m.Debugger.loc != m.Debugger.prevLoc { m.Debugger.state = DebugAtCmd m.Debugger.prevLoc = m.Debugger.loc @@ -262,8 +262,8 @@ func debugBreak(m *Machine, arg string) error { if err != nil { return err } - m.breakpoints = append(m.breakpoints, loc) - printBreakpoint(m, len(m.breakpoints)-1) + m.Debugger.breakpoints = append(m.Debugger.breakpoints, loc) + printBreakpoint(m, len(m.Debugger.breakpoints)-1) return nil } @@ -318,7 +318,7 @@ const ( ) func debugBreakpoints(m *Machine, arg string) error { - for i := range m.breakpoints { + for i := range m.Debugger.breakpoints { printBreakpoint(m, i) } return nil @@ -333,13 +333,13 @@ const ( func debugClear(m *Machine, arg string) error { if arg != "" { id, err := strconv.Atoi(arg) - if err != nil || id < 0 || id >= len(m.breakpoints) { + if err != nil || id < 0 || id >= len(m.Debugger.breakpoints) { return fmt.Errorf("invalid breakpoint id: %v", arg) } - m.breakpoints = append(m.breakpoints[:id], m.breakpoints[id+1:]...) + m.Debugger.breakpoints = append(m.Debugger.breakpoints[:id], m.Debugger.breakpoints[id+1:]...) return nil } - m.breakpoints = nil + m.Debugger.breakpoints = nil return nil } @@ -406,7 +406,7 @@ const ( exitShort = `Exit the debugger and program.` ) -func debugExit(m *Machine, arg string) error { m.state = DebugAtExit; return nil } +func debugExit(m *Machine, arg string) error { m.Debugger.state = DebugAtExit; return nil } // --------------------------------------- const ( diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 8ca9ad5101b..7cc55ee6b9a 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -51,7 +51,7 @@ type Machine struct { NumResults int // number of results returned Cycles int64 // number of "cpu" cycles - Debugger + Debugger Debugger // Configuration CheckTypes bool // not yet used diff --git a/gnovm/tests/integ/debugger/sample2.gno b/gnovm/tests/integ/debugger/sample2.gno new file mode 100644 index 00000000000..d2887cc8ee5 --- /dev/null +++ b/gnovm/tests/integ/debugger/sample2.gno @@ -0,0 +1,8 @@ +package main + +import "gno.land/p/demo/tests" + +func main() { + b := tests.World + println("b:", b) +} From 1c52ee6e0ceabf7b272a15955a8d17abbfa39f4d Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 14 May 2024 21:18:16 +0200 Subject: [PATCH 37/37] fix invalid test --- gnovm/cmd/gno/main_test.go | 4 ++-- gnovm/cmd/gno/run_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gnovm/cmd/gno/main_test.go b/gnovm/cmd/gno/main_test.go index 1395d120012..1797d0aede9 100644 --- a/gnovm/cmd/gno/main_test.go +++ b/gnovm/cmd/gno/main_test.go @@ -90,7 +90,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { if r := recover(); r != nil { output := fmt.Sprintf("%v", r) t.Log("recover", output) - require.False(t, recoverShouldBeEmpty, "should panic") + require.False(t, recoverShouldBeEmpty, "should not panic") require.True(t, errShouldBeEmpty, "should not return an error") if test.recoverShouldContain != "" { require.Regexpf(t, test.recoverShouldContain, output, "recover should contain") @@ -100,7 +100,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { } checkOutputs(t) } else { - require.True(t, recoverShouldBeEmpty, "should not panic") + require.True(t, recoverShouldBeEmpty, "should panic") } }() diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index e090308b89d..f78c15edb34 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -72,8 +72,8 @@ func TestRunApp(t *testing.T) { stdoutShouldContain: "Welcome to the Gnovm debugger", }, { - args: []string{"run", "-debug-addr", "invalidhost:17538", "../../tests/integ/debugger/sample.gno"}, - recoverShouldContain: "listen tcp: lookup invalidhost", + args: []string{"run", "-debug-addr", "invalidhost:17538", "../../tests/integ/debugger/sample.gno"}, + errShouldContain: "listen tcp: lookup invalidhost", }, // TODO: a test file // TODO: args