//go:build !windows // 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" "strconv" "strings" "syscall" "unsafe" ) // Terminal mode helpers — restore cooked mode for debugger, re-enter raw for program var savedTermios syscall.Termios var termSaved bool func restoreCooked() { fd := int(os.Stdin.Fd()) var t syscall.Termios syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t)), 0, 0, 0) if !termSaved { savedTermios = t termSaved = true } // Set cooked mode t.Lflag |= syscall.ICANON | syscall.ECHO t.Oflag |= syscall.OPOST syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t)), 0, 0, 0) } func reenterRaw() { fd := int(os.Stdin.Fd()) var t syscall.Termios syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t)), 0, 0, 0) t.Lflag &^= syscall.ICANON | syscall.ECHO | syscall.ISIG t.Oflag &^= syscall.OPOST t.Cc[syscall.VMIN] = 1 t.Cc[syscall.VTIME] = 0 syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t)), 0, 0, 0) } // 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) for { fmt.Printf("(dbg) ") line, err := reader.ReadString('\n') if err != nil { return DbgContinue } line = strings.TrimSpace(line) if line == "" { line = lastCmd // repeat last command } else { lastCmd = line } parts := strings.Fields(line) cmd := parts[0] switch cmd { case "s", "step": return DbgStepLine case "n", "next": return DbgStepOver case "o", "out": return DbgStepOut case "c", "cont", "continue": return DbgContinue case "b", "break": if len(parts) >= 2 { lineNo, err := strconv.Atoi(parts[1]) if err == nil { mod := event.Module if len(parts) >= 3 { mod = parts[2] } dbg := event.Thread.VM().Debugger idx := dbg.AddBreakpoint(mod, lineNo) fmt.Printf(" Breakpoint %d at %s:%d\n", idx, mod, lineNo) } else { fmt.Println(" Usage: b [module]") } } else { fmt.Println(" Usage: b [module]") } case "d", "del", "delete": if len(parts) >= 2 { idx, err := strconv.Atoi(parts[1]) if err == nil { event.Thread.VM().Debugger.RemoveBreakpoint(idx) fmt.Printf(" Breakpoint %d removed\n", idx) } } else { fmt.Println(" Usage: d ") } case "bl", "breakpoints": dbg := event.Thread.VM().Debugger if len(dbg.Breakpoints) == 0 { fmt.Println(" No breakpoints") } else { for i, bp := range dbg.Breakpoints { status := "ON " if !bp.Enabled { status = "OFF" } fmt.Printf(" %d: [%s] %s:%d (hits: %d)\n", i, status, bp.Module, bp.Line, bp.HitCount) } } case "l", "locals": if len(event.Locals) == 0 { fmt.Println(" No local variables") } else { for _, v := range event.Locals { fmt.Printf(" %s [%s] %s = %s\n", v.Scope, fmt.Sprintf("%d", v.Index), v.Name, v.Value.String()) } } case "p", "print": if len(parts) >= 2 { varName := parts[1] found := false for _, v := range event.Locals { if strings.EqualFold(v.Name, varName) || fmt.Sprintf("_%d", v.Index) == varName { fmt.Printf(" %s = %s\n", v.Name, v.Value.String()) found = true break } } if !found { // Try by index idx, err := strconv.Atoi(varName) if err == nil && idx >= 1 && idx <= len(event.Locals) { v := event.Locals[idx-1] fmt.Printf(" %s = %s\n", v.Name, v.Value.String()) } else { fmt.Printf(" Variable '%s' not found\n", varName) } } } else { fmt.Println(" Usage: p ") } case "bt", "backtrace", "stack": if len(event.CallStack) == 0 { fmt.Println(" Empty call stack") } else { for i, frame := range event.CallStack { marker := " " if i == 0 { marker = "=>" } if frame.Module != "" { fmt.Printf(" %s #%d %s() at %s:%d\n", marker, frame.Level, frame.Function, frame.Module, frame.Line) } else { fmt.Printf(" %s #%d %s()\n", marker, frame.Level, frame.Function) } } } case "q", "quit": fmt.Println(" Debugger quit.") os.Exit(0) case "h", "help", "?": fmt.Println(" Five Debugger Commands:") fmt.Println(" s, step — step to next line") fmt.Println(" n, next — step over function calls") fmt.Println(" o, out — step out of current function") fmt.Println(" c, cont — continue (run to next breakpoint)") fmt.Println(" b — set breakpoint at line") fmt.Println(" d — delete breakpoint n") fmt.Println(" bl — list all breakpoints") fmt.Println(" l — show local variables") fmt.Println(" p — print variable value") fmt.Println(" bt — show call stack") fmt.Println(" q — quit") default: fmt.Printf(" Unknown command: %s (type 'h' for help)\n", cmd) } } } } // 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]) } } }