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:
131
cmd/five/main.go
131
cmd/five/main.go
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -70,9 +71,41 @@ func main() {
|
||||
genPRG(os.Args[2])
|
||||
case "debug":
|
||||
if len(os.Args) < 3 {
|
||||
fatal("usage: five debug <file.prg>")
|
||||
fatal("usage: five debug <file.prg> [-b [module:]line ...] [--cli]")
|
||||
}
|
||||
debugPRG(os.Args[2])
|
||||
prg := ""
|
||||
var breakpoints []string
|
||||
var watches []string
|
||||
useCLI := false
|
||||
for i := 2; i < len(os.Args); i++ {
|
||||
a := os.Args[i]
|
||||
switch a {
|
||||
case "-b", "--break":
|
||||
if i+1 >= len(os.Args) {
|
||||
fatal("-b requires an argument: [module:]line")
|
||||
}
|
||||
breakpoints = append(breakpoints, os.Args[i+1])
|
||||
i++
|
||||
case "-w", "--watch":
|
||||
if i+1 >= len(os.Args) {
|
||||
fatal("-w requires an argument: <expr>")
|
||||
}
|
||||
watches = append(watches, os.Args[i+1])
|
||||
i++
|
||||
case "--cli":
|
||||
useCLI = true
|
||||
default:
|
||||
if prg == "" {
|
||||
prg = a
|
||||
} else {
|
||||
fatal("unexpected argument: " + a)
|
||||
}
|
||||
}
|
||||
}
|
||||
if prg == "" {
|
||||
fatal("usage: five debug <file.prg> [-b [module:]line ...] [-w <expr> ...] [--cli]")
|
||||
}
|
||||
debugPRGWithOpts(prg, breakpoints, watches, useCLI)
|
||||
case "frb":
|
||||
if len(os.Args) < 3 {
|
||||
fatal("usage: five frb <file.prg> [-o output.frb] [--pcode]")
|
||||
@@ -177,8 +210,8 @@ func buildPRG(prgFile, output string) {
|
||||
fatal("cannot resolve output path: " + err.Error())
|
||||
}
|
||||
|
||||
// go build
|
||||
cmd := exec.Command(goPath(), "build", "-o", absOutput, ".")
|
||||
// go build — pgoArgs() adds -pgo=default.pgo when available.
|
||||
cmd := exec.Command(goPath(), buildArgs(absOutput)...)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -270,7 +303,7 @@ func buildMultiPRG(prgFiles []string, output string) {
|
||||
if err != nil {
|
||||
fatal("cannot resolve output path: " + err.Error())
|
||||
}
|
||||
cmd := exec.Command(goPath(), "build", "-o", absOutput, ".")
|
||||
cmd := exec.Command(goPath(), buildArgs(absOutput)...)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -527,6 +560,31 @@ func walkUpForGoMod(startDir string) string {
|
||||
// goPath is an alias for findGoBin (deduplicated).
|
||||
func goPath() string { return findGoBin() }
|
||||
|
||||
// pgoArgs returns ["-pgo=<path>"] when the Five project root contains a
|
||||
// default.pgo file — profile-guided compilation. Empty otherwise, so
|
||||
// builds proceed without PGO when the profile hasn't been collected.
|
||||
// The FIVE_NO_PGO env var forces it off (useful when collecting a new
|
||||
// profile or A/B benchmarking).
|
||||
func pgoArgs() []string {
|
||||
if os.Getenv("FIVE_NO_PGO") != "" {
|
||||
return nil
|
||||
}
|
||||
root := findFiveRoot()
|
||||
p := filepath.Join(root, "default.pgo")
|
||||
if fi, err := os.Stat(p); err == nil && !fi.IsDir() && fi.Size() > 0 {
|
||||
return []string{"-pgo=" + p}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildArgs composes the full args for a `go build` invocation,
|
||||
// inserting -pgo when a profile is available.
|
||||
func buildArgs(output string) []string {
|
||||
args := []string{"build"}
|
||||
args = append(args, pgoArgs()...)
|
||||
return append(args, "-o", output, ".")
|
||||
}
|
||||
|
||||
func writeFile(path, content string) {
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
fatal("cannot write " + path + ": " + err.Error())
|
||||
@@ -651,8 +709,33 @@ func buildFRB(prgFile, outputFile string) {
|
||||
fmt.Fprintf(os.Stderr, "FRB: %s (%d bytes)\n", outputFile, len(frbData))
|
||||
}
|
||||
|
||||
// debugPRG compiles PRG with debug info and runs with interactive debugger.
|
||||
func debugPRG(prgFile string) {
|
||||
// debugPRG is kept as a thin wrapper for backward-compatibility — no
|
||||
// pre-launch breakpoints, TUI frontend.
|
||||
func debugPRG(prgFile string) { debugPRGWithOpts(prgFile, nil, nil, false) }
|
||||
|
||||
// parseBPSpec parses "[module:]line" into (module, line, ok). A bare
|
||||
// "42" uses defaultMod. Colons inside module (Windows paths) aren't
|
||||
// supported — use forward slashes.
|
||||
func parseBPSpec(spec, defaultMod string) (string, int, bool) {
|
||||
mod := defaultMod
|
||||
lineStr := spec
|
||||
if i := strings.LastIndex(spec, ":"); i > 0 {
|
||||
mod = spec[:i]
|
||||
lineStr = spec[i+1:]
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(lineStr))
|
||||
if err != nil || n <= 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
return mod, n, true
|
||||
}
|
||||
|
||||
// debugPRGWithOpts compiles PRG with debug info and runs with interactive
|
||||
// debugger. breakpoints is a list of "[module:]line" strings (module
|
||||
// defaults to the PRG's basename). watches is a list of PRG expressions
|
||||
// auto-evaluated at each stop. useCLI picks the gdb-style CLI frontend
|
||||
// instead of the full-screen TUI.
|
||||
func debugPRGWithOpts(prgFile string, breakpoints, watches []string, useCLI bool) {
|
||||
source, err := os.ReadFile(prgFile)
|
||||
if err != nil {
|
||||
fatal("cannot read file: " + err.Error())
|
||||
@@ -700,10 +783,38 @@ func debugPRG(prgFile string) {
|
||||
goMod := fmt.Sprintf("module five-generated\n\ngo 1.21.13\n\nrequire five v0.0.0\n\nreplace five => %s\n", fiveRoot)
|
||||
writeFile(filepath.Join(tmpDir, "go.mod"), goMod)
|
||||
|
||||
// Add debug setup to main (use %q for safe path escaping)
|
||||
// Build the debug setup: create Debugger, register any pre-launch
|
||||
// breakpoints + watches, choose frontend, then run.
|
||||
callback := "hbrt.TUIDebugger()"
|
||||
if useCLI {
|
||||
callback = "hbrt.CLIDebugger()"
|
||||
}
|
||||
prgBase := filepath.Base(prgFile)
|
||||
var setupLines []string
|
||||
for _, spec := range breakpoints {
|
||||
mod, line, ok := parseBPSpec(spec, prgBase)
|
||||
if !ok {
|
||||
fatal(fmt.Sprintf("invalid -b %q — expected [module:]line", spec))
|
||||
}
|
||||
setupLines = append(setupLines,
|
||||
fmt.Sprintf("vm.Debugger.AddBreakpoint(%q, %d)", mod, line))
|
||||
}
|
||||
for _, w := range watches {
|
||||
setupLines = append(setupLines,
|
||||
fmt.Sprintf("vm.Debugger.Watches = append(vm.Debugger.Watches, %q)", w))
|
||||
}
|
||||
// If any breakpoints were set, start in Continue mode so the program
|
||||
// runs until it hits one. Otherwise keep step-line (legacy behavior).
|
||||
startMode := "hbrt.DbgStepLine"
|
||||
if len(breakpoints) > 0 {
|
||||
startMode = "hbrt.DbgContinue"
|
||||
}
|
||||
debugSetup := fmt.Sprintf(
|
||||
"vm.Debugger = hbrt.NewDebugger()\n\tvm.Debugger.SourceDir = %s\n\tvm.Debugger.Callback = hbrt.TUIDebugger()\n\tvm.Run(\"MAIN\")",
|
||||
fmt.Sprintf("%q", mustAbs(".")))
|
||||
"vm.Debugger = hbrt.NewDebugger()\n\tvm.Debugger.SourceDir = %s\n\tvm.Debugger.Mode = %s\n\tvm.Debugger.Callback = %s\n\t%s\n\tvm.Run(\"MAIN\")",
|
||||
fmt.Sprintf("%q", mustAbs(".")),
|
||||
startMode,
|
||||
callback,
|
||||
strings.Join(setupLines, "\n\t"))
|
||||
goSrc = strings.Replace(goSrc, "vm.Run(\"MAIN\")", debugSetup, 1)
|
||||
// Remove unused fmt import if added
|
||||
// (no longer needed since we don't use fmt.Println in generated code)
|
||||
|
||||
Reference in New Issue
Block a user