- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
254 lines
6.8 KiB
Go
254 lines
6.8 KiB
Go
// 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 <line> — set breakpoint at line
|
|
// d <n> — delete breakpoint
|
|
// bl — list breakpoints
|
|
// p <expr> — 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 <line> [module]")
|
|
}
|
|
} else {
|
|
fmt.Println(" Usage: b <line> [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 <breakpoint_number>")
|
|
}
|
|
|
|
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 <varname|index>")
|
|
}
|
|
|
|
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 <line> — set breakpoint at line")
|
|
fmt.Println(" d <n> — delete breakpoint n")
|
|
fmt.Println(" bl — list all breakpoints")
|
|
fmt.Println(" l — show local variables")
|
|
fmt.Println(" p <var> — 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])
|
|
}
|
|
}
|
|
}
|