checkpoint: season-wide bug fix campaign + infra
Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
193
hbrt/debugcli.go
193
hbrt/debugcli.go
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||||
// All rights reserved.
|
||||
|
||||
@@ -26,40 +24,11 @@ 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)
|
||||
}
|
||||
// Terminal raw/cooked mode helpers live in termios_<os>.go — their ioctl
|
||||
// numbers differ per platform (Linux TCGETS vs macOS TIOCGETA).
|
||||
|
||||
// CLIDebugger creates a DebugCallback for interactive terminal debugging.
|
||||
func CLIDebugger() DebugCallback {
|
||||
@@ -80,6 +49,26 @@ func CLIDebugger() DebugCallback {
|
||||
// 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')
|
||||
@@ -88,141 +77,14 @@ func CLIDebugger() DebugCallback {
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
line = lastCmd // repeat last command
|
||||
line = lastCmd
|
||||
} 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)
|
||||
mode := runDebugCmd(event, line, func(s string) { fmt.Println(s) })
|
||||
if mode != cmdNoMode {
|
||||
return mode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,3 +115,6 @@ func showSourceLine(module string, line int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared helpers (parseBreakArgs, describeDbgValue, runDebugCmd) now
|
||||
// live in debugcmd.go so the TUI can reuse them.
|
||||
|
||||
Reference in New Issue
Block a user