// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Interactive CLI debugger for Five. // Provides a gdb-like command interface for stepping through PRG code. // // Commands: // s, step — step to next line // n, next — step over (don't enter functions) // o, out — step out of current function // c, cont — continue (run until breakpoint) // b — set breakpoint at line // d — delete breakpoint // bl — list breakpoints // p — print variable value // l — list locals // bt — show call stack (backtrace) // q — quit // h — help package hbrt import ( "bufio" "fmt" "os" "strings" ) // Terminal raw/cooked mode helpers live in termios_.go — their ioctl // numbers differ per platform (Linux TCGETS vs macOS TIOCGETA). // CLIDebugger creates a DebugCallback for interactive terminal debugging. func CLIDebugger() DebugCallback { reader := bufio.NewReader(os.Stdin) lastCmd := "s" // default repeat command return func(event *DebugEvent) int { // Restore terminal to cooked mode for debugger I/O fmt.Print("\r\n") restoreCooked() defer reenterRaw() if event.Reason == "breakpoint" { fmt.Printf(" ** Breakpoint at %s:%d\n", event.Module, event.Line) } fmt.Printf(" %s:%d in %s()\n", event.Module, event.Line, event.Function) // Show source line if available showSourceLine(event.Module, event.Line) // Auto-eval watches. Each watch expression is shown alongside // its current value so the user doesn't have to re-type them // after every step. Failed watches show the eval error inline // instead of the value. dbgForWatch := event.Thread.VM().Debugger if dbgForWatch != nil && len(dbgForWatch.Watches) > 0 { fmt.Println(" -- watches --") for i, expr := range dbgForWatch.Watches { v, evalErr := event.Thread.EvalWithFrameLocals(expr) if evalErr != "" { fmt.Printf(" [%d] %s ! %s\n", i, expr, evalErr) } else { fmt.Printf(" [%d] %s = %s\n", i, expr, describeDbgValue(v)) } } } // CLI prompt loop — delegates each input line to runDebugCmd. // runDebugCmd returns a Dbg* mode to resume execution, or // cmdNoMode (-1) when the command just printed output. for { fmt.Printf("(dbg) ") line, err := reader.ReadString('\n') if err != nil { return DbgContinue } line = strings.TrimSpace(line) if line == "" { line = lastCmd } else { lastCmd = line } mode := runDebugCmd(event, line, func(s string) { fmt.Println(s) }) if mode != cmdNoMode { return mode } } } } // showSourceLine attempts to show the source code around the current line. func showSourceLine(module string, line int) { data, err := os.ReadFile(module) if err != nil { return } lines := strings.Split(string(data), "\n") start := line - 3 if start < 1 { start = 1 } end := line + 2 if end > len(lines) { end = len(lines) } for i := start; i <= end; i++ { marker := " " if i == line { marker = ">>" } if i-1 < len(lines) { fmt.Printf(" %s %4d: %s\n", marker, i, lines[i-1]) } } } // Shared helpers (parseBreakArgs, describeDbgValue, runDebugCmd) now // live in debugcmd.go so the TUI can reuse them.