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:
2026-04-30 09:26:25 +09:00
parent 8a3f296e9a
commit f4ed42556b
63 changed files with 10486 additions and 2740 deletions

View File

@@ -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)