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>
388 lines
10 KiB
Go
388 lines
10 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
||
// All rights reserved.
|
||
|
||
// Shared debugger command dispatch. Both the CLI (gdb-style prompt)
|
||
// and the TUI (full-screen F-keys + `:` prompt) funnel parsed input
|
||
// strings through runDebugCmd so the surface area stays in one place.
|
||
|
||
package hbrt
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// cmdNoMode is the sentinel returned by runDebugCmd when a command
|
||
// printed output and the debug loop should keep prompting — i.e. no
|
||
// mode transition is requested.
|
||
const cmdNoMode = -1
|
||
|
||
// runDebugCmd interprets one line of debugger input against the given
|
||
// event. Returns a DbgContinue / DbgStepLine / ... constant when the
|
||
// command resumes execution, or cmdNoMode to stay in the prompt loop.
|
||
// Prints results/errors using the supplied `out` writer so the TUI can
|
||
// buffer them for its status area while the CLI writes to stdout.
|
||
func runDebugCmd(event *DebugEvent, line string, out func(string)) int {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
return cmdNoMode
|
||
}
|
||
if out == nil {
|
||
out = func(s string) { fmt.Println(s) }
|
||
}
|
||
|
||
parts := strings.Fields(line)
|
||
cmd := parts[0]
|
||
dbg := event.Thread.VM().Debugger
|
||
|
||
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":
|
||
mod, lineNo, cond, ok := parseBreakArgs(line, parts, event.Module)
|
||
if !ok {
|
||
out(" Usage: b <line> [module] [if <expr>]")
|
||
return cmdNoMode
|
||
}
|
||
idx := dbg.AddBreakpoint(mod, lineNo)
|
||
if cond != "" {
|
||
dbg.Breakpoints[idx].Condition = cond
|
||
out(fmt.Sprintf(" Breakpoint %d at %s:%d if %s", idx, mod, lineNo, cond))
|
||
} else {
|
||
out(fmt.Sprintf(" Breakpoint %d at %s:%d", idx, mod, lineNo))
|
||
}
|
||
|
||
case "u", "until":
|
||
if len(parts) >= 2 {
|
||
if lineNo, err := strconv.Atoi(parts[1]); err == nil && lineNo > 0 {
|
||
dbg.ToCursorMod = event.Module
|
||
dbg.ToCursorLine = lineNo
|
||
if len(parts) >= 3 {
|
||
dbg.ToCursorMod = parts[2]
|
||
}
|
||
return DbgToCursor
|
||
}
|
||
}
|
||
out(" Usage: u <line> [module]")
|
||
|
||
case "w", "watch":
|
||
if len(parts) < 2 {
|
||
if len(dbg.Watches) == 0 {
|
||
out(" No watches. Usage: w <expr>")
|
||
} else {
|
||
for i, e := range dbg.Watches {
|
||
out(fmt.Sprintf(" [%d] %s", i, e))
|
||
}
|
||
}
|
||
return cmdNoMode
|
||
}
|
||
expr := strings.TrimSpace(line[len(parts[0]):])
|
||
dbg.Watches = append(dbg.Watches, expr)
|
||
out(fmt.Sprintf(" Watch %d: %s", len(dbg.Watches)-1, expr))
|
||
|
||
case "wd", "unwatch":
|
||
if len(parts) >= 2 {
|
||
if idx, err := strconv.Atoi(parts[1]); err == nil {
|
||
if idx >= 0 && idx < len(dbg.Watches) {
|
||
dbg.Watches = append(dbg.Watches[:idx], dbg.Watches[idx+1:]...)
|
||
out(fmt.Sprintf(" Watch %d removed", idx))
|
||
return cmdNoMode
|
||
}
|
||
}
|
||
}
|
||
out(" Usage: wd <watch_number>")
|
||
|
||
case "d", "del", "delete":
|
||
if len(parts) >= 2 {
|
||
if idx, err := strconv.Atoi(parts[1]); err == nil {
|
||
dbg.RemoveBreakpoint(idx)
|
||
out(fmt.Sprintf(" Breakpoint %d removed", idx))
|
||
return cmdNoMode
|
||
}
|
||
}
|
||
out(" Usage: d <breakpoint_number>")
|
||
|
||
case "bl", "breakpoints":
|
||
if len(dbg.Breakpoints) == 0 {
|
||
out(" No breakpoints")
|
||
} else {
|
||
for i, bp := range dbg.Breakpoints {
|
||
status := "ON "
|
||
if !bp.Enabled {
|
||
status = "OFF"
|
||
}
|
||
cond := ""
|
||
if bp.Condition != "" {
|
||
cond = " if " + bp.Condition
|
||
}
|
||
out(fmt.Sprintf(" %d: [%s] %s:%d%s (hits: %d)",
|
||
i, status, bp.Module, bp.Line, cond, bp.HitCount))
|
||
}
|
||
}
|
||
|
||
case "l", "locals":
|
||
if len(event.Locals) == 0 {
|
||
out(" No local variables")
|
||
} else {
|
||
for _, v := range event.Locals {
|
||
out(fmt.Sprintf(" %s [%d] %s = %s",
|
||
v.Scope, v.Index, v.Name, describeDbgValue(v.Value)))
|
||
}
|
||
}
|
||
|
||
case "p", "print":
|
||
if len(parts) < 2 {
|
||
out(" Usage: p <expr>")
|
||
return cmdNoMode
|
||
}
|
||
expr := strings.TrimSpace(line[len(parts[0]):])
|
||
v, evalErr := event.Thread.EvalWithFrameLocals(expr)
|
||
if evalErr != "" {
|
||
out(fmt.Sprintf(" eval failed: %s", evalErr))
|
||
} else {
|
||
out(fmt.Sprintf(" %s = %s", expr, describeDbgValue(v)))
|
||
}
|
||
|
||
case "diag", "d!":
|
||
// Full error.log-style dump at the break point — workareas,
|
||
// SET flags, runtime memory. Same renderer our DefaultErrorHook
|
||
// uses, so what you see here is what you'd get if the program
|
||
// had crashed instead of stopped.
|
||
if DebugDiagnosticHook == nil {
|
||
out(" (diagnostics unavailable — hook not installed)")
|
||
} else {
|
||
DebugDiagnosticHook(event.Thread, "", func(s string) {
|
||
for _, ln := range strings.Split(s, "\n") {
|
||
if ln != "" {
|
||
out(ln)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
case "wa", "workareas":
|
||
if DebugDiagnosticHook == nil {
|
||
out(" (workarea info unavailable)")
|
||
} else {
|
||
DebugDiagnosticHook(event.Thread, "wa", func(s string) {
|
||
for _, ln := range strings.Split(s, "\n") {
|
||
if ln != "" {
|
||
out(ln)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
case "set":
|
||
if DebugDiagnosticHook == nil {
|
||
out(" (SET state unavailable)")
|
||
} else {
|
||
DebugDiagnosticHook(event.Thread, "set", func(s string) {
|
||
for _, ln := range strings.Split(s, "\n") {
|
||
if ln != "" {
|
||
out(ln)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
case "mem":
|
||
if DebugDiagnosticHook == nil {
|
||
out(" (memory stats unavailable)")
|
||
} else {
|
||
DebugDiagnosticHook(event.Thread, "mem", func(s string) {
|
||
for _, ln := range strings.Split(s, "\n") {
|
||
if ln != "" {
|
||
out(ln)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
case "hist", "trace":
|
||
// Execution trace — last N PRG lines the current thread stepped
|
||
// through. Most useful for "how did control reach here?" when
|
||
// you break inside a deeply-nested helper and want to see the
|
||
// LOOP/IF/Call chain that led to it. Deduplicates adjacent
|
||
// repeats (tight FOR loops compress to "line X ×27") so the
|
||
// output stays readable in hot code.
|
||
trace, total := event.Thread.Trace()
|
||
if len(trace) == 0 {
|
||
out(" (no trace data — debugger just attached?)")
|
||
return cmdNoMode
|
||
}
|
||
// How many to show — default 50, or "hist N"
|
||
limit := 50
|
||
if len(parts) >= 2 {
|
||
if n, err := strconv.Atoi(parts[1]); err == nil && n > 0 {
|
||
limit = n
|
||
}
|
||
}
|
||
if limit > len(trace) {
|
||
limit = len(trace)
|
||
}
|
||
view := trace[len(trace)-limit:]
|
||
// Collapse adjacent duplicates.
|
||
type run struct {
|
||
e TraceEntry
|
||
count int
|
||
}
|
||
var runs []run
|
||
for _, e := range view {
|
||
if n := len(runs); n > 0 && runs[n-1].e == e {
|
||
runs[n-1].count++
|
||
continue
|
||
}
|
||
runs = append(runs, run{e, 1})
|
||
}
|
||
out(fmt.Sprintf(" trace (last %d of %d) — newest last:", len(view), total))
|
||
for _, r := range runs {
|
||
suffix := ""
|
||
if r.count > 1 {
|
||
suffix = fmt.Sprintf(" ×%d", r.count)
|
||
}
|
||
out(fmt.Sprintf(" %s:%d%s", r.e.Module, r.e.Line, suffix))
|
||
}
|
||
|
||
case "threads", "ts":
|
||
// Lists every live Thread managed by the VM with its current
|
||
// PRG source position. Useful for diagnosing multi-thread PRG
|
||
// programs (hb_Thread*, GoLaunch) where the debugger is
|
||
// currently attached to one thread but others may be blocked,
|
||
// looping, or crashed. Position is read from each thread's
|
||
// current frame — may show an older line for threads that
|
||
// haven't executed a DebugLine recently.
|
||
threads := event.Thread.VM().Threads()
|
||
if len(threads) == 0 {
|
||
out(" (no threads tracked)")
|
||
return cmdNoMode
|
||
}
|
||
for _, th := range threads {
|
||
marker := " "
|
||
if th == event.Thread {
|
||
marker = "=>"
|
||
}
|
||
mod, line := "", 0
|
||
if f := th.CurFrame(); f != nil {
|
||
mod = f.module
|
||
line = f.line
|
||
}
|
||
name := "MAIN"
|
||
if f := th.CurFrame(); f != nil && f.symbol != nil {
|
||
name = f.symbol.Name
|
||
}
|
||
if mod == "" {
|
||
out(fmt.Sprintf(" %s [%d] %s", marker, th.TID(), name))
|
||
} else {
|
||
out(fmt.Sprintf(" %s [%d] %s at %s:%d",
|
||
marker, th.TID(), name, mod, line))
|
||
}
|
||
}
|
||
|
||
case "bt", "backtrace", "stack":
|
||
if len(event.CallStack) == 0 {
|
||
out(" Empty call stack")
|
||
} else {
|
||
for i, frame := range event.CallStack {
|
||
marker := " "
|
||
if i == 0 {
|
||
marker = "=>"
|
||
}
|
||
if frame.Module != "" {
|
||
out(fmt.Sprintf(" %s #%d %s() at %s:%d",
|
||
marker, frame.Level, frame.Function, frame.Module, frame.Line))
|
||
} else {
|
||
out(fmt.Sprintf(" %s #%d %s()", marker, frame.Level, frame.Function))
|
||
}
|
||
}
|
||
}
|
||
|
||
case "q", "quit":
|
||
out(" Debugger quit.")
|
||
os.Exit(0)
|
||
|
||
case "h", "help", "?":
|
||
for _, ln := range debugCmdHelp {
|
||
out(ln)
|
||
}
|
||
|
||
default:
|
||
out(fmt.Sprintf(" Unknown command: %s (type 'h' for help)", cmd))
|
||
}
|
||
return cmdNoMode
|
||
}
|
||
|
||
var debugCmdHelp = []string{
|
||
" Five Debugger Commands:",
|
||
" s, step — step to next line",
|
||
" n, next — step over function calls",
|
||
" o, out — step out of current function",
|
||
" c, cont — continue (run to next breakpoint)",
|
||
" u <line> — run until <line> in current module",
|
||
" b <line> [if E] — set breakpoint, optional condition",
|
||
" d <n> — delete breakpoint n",
|
||
" bl — list all breakpoints",
|
||
" w <expr> — add watch expression",
|
||
" wd <n> — remove watch n",
|
||
" w — list watches",
|
||
" l — show local variables",
|
||
" p <expr> — evaluate and print expression",
|
||
" bt — show call stack",
|
||
" wa — list open workareas + active index",
|
||
" set — SET state (DELETED, DATEFORMAT, ...)",
|
||
" mem — runtime memory / GC stats",
|
||
" diag — full diag dump (wa + set + mem)",
|
||
" threads, ts — list all live threads",
|
||
" hist, trace [N] — last N lines executed (how did we get here?)",
|
||
" q — quit",
|
||
}
|
||
|
||
// describeDbgValue is now shared across platforms. It lives in
|
||
// debugcmd.go because debugcli.go is !windows-only and runDebugCmd
|
||
// needs this regardless of platform.
|
||
func describeDbgValue(v Value) string {
|
||
switch {
|
||
case v.IsNil():
|
||
return "NIL"
|
||
case v.IsString():
|
||
return fmt.Sprintf("%q", v.AsString())
|
||
}
|
||
return v.String()
|
||
}
|
||
|
||
// parseBreakArgs accepts:
|
||
//
|
||
// b <line>
|
||
// b <line> <module>
|
||
// b <line> if <expr>
|
||
// b <line> <module> if <expr>
|
||
func parseBreakArgs(rawLine string, parts []string, defaultMod string) (module string, line int, cond string, ok bool) {
|
||
if len(parts) < 2 {
|
||
return "", 0, "", false
|
||
}
|
||
lineNo, err := strconv.Atoi(parts[1])
|
||
if err != nil || lineNo <= 0 {
|
||
return "", 0, "", false
|
||
}
|
||
module = defaultMod
|
||
lowered := strings.ToLower(rawLine)
|
||
if idx := strings.Index(lowered, " if "); idx > 0 {
|
||
cond = strings.TrimSpace(rawLine[idx+4:])
|
||
rawLine = rawLine[:idx]
|
||
parts = strings.Fields(rawLine)
|
||
}
|
||
if len(parts) >= 3 {
|
||
module = parts[2]
|
||
}
|
||
return module, lineNo, cond, true
|
||
}
|