Commit Graph

39 Commits

Author SHA1 Message Date
bfc6ded8cb perf(FiveSql2): SqlHashBuild + FetchRow column binding — 3-way JOIN 3x
Complex-query benchmarking turned up two hot paths that the earlier
SqlScan/SqlEach work didn't touch: multi-table JOIN and nested-scan
row fetching. This commit hits both.

--- Part 1: SqlHashBuild — Go-native hash-join build ---

FiveSql2's HashJoin previously built the inner-side hash in PRG:

    WHILE !Eof()
      xVal := FieldGet(nFPos)
      cKey := SqlValToStr(xVal)
      IF !hb_HHasKey(hHash, cKey) ; hHash[cKey] := {} ; ENDIF
      AAdd(hHash[cKey], RecNo())
      dbSkip()
    ENDDO

That loop runs at ~40μs per row from class dispatch + hb_HHasKey
lookups + AAdd growth + SqlValToStr formatting. On a 50k-row inner
table that's ~2 seconds wasted on what should be a sub-50ms
housekeeping op.

New hbrtl.SqlHashBuild does the same thing in one Go-native pass:

  - Direct *dbf.DBFArea loop (no interface dispatch, same devirt as
    SqlScan)
  - Go `map[string][]int64` accumulates RecNos by key — one
    allocation per distinct key
  - Inline ASCII-only digit formatter for numeric keys (strconv.Itoa
    is allocation-heavy for small ints)
  - CHAR keys are right-trimmed to match SqlCmpEq semantics so the
    hash probe matches what EvalExpr would compute
  - Final Five hash is built once from Keys/Values/Order slices
    directly, skipping the per-key hb_HSet path

HashJoin now calls `SqlHashBuild(nFPos)` instead of running the
PRG loop.

--- Part 2: TSqlExecutor:BuildFetchCache ---

The JOIN fallback loop calls FetchRow per row. FetchRow was already
column-ref-aware but did the string parse (`At + SubStr + Upper`)
and `::FindWA` linear scan every single invocation. For a 50k-row
join emitting 50k result rows, that's ~200k redundant resolutions.

New BuildFetchCache walks the SELECT list once before the scan and
pre-binds each plain-column expression to `{nWA, nFPos}`. FetchRow's
new fast path checks ::aFetchCache and jumps straight to
`dbSelectArea + FieldGet` when bound. Complex exprs (functions,
CASE, subqueries) still fall through to EvalExpr.

::aFetchCache is set right before the join WHILE loop and cleared
after — no cross-query bleed.

--- Bench (50k ord × 10k emp × 100 dept, 3-run steady state) ---

  Query                        Before      After     Speedup
  ────────────────────────────────────────────────────────────
  2-way INNER JOIN, 10k rows   91ms        68ms      1.34x
  2-way JOIN + GROUP BY        110ms       94ms      1.17x
  3-way INNER JOIN COUNT       2610ms      610ms     4.28x
  3-way JOIN + GROUP BY        2860ms      830ms     3.45x

The 3-way speedup is almost entirely SqlHashBuild. The 2-way case
benefits from the fetch cache because its per-row cost is dominated
by FetchRow (no second hash build to amortize).

--- Limits still standing ---

CTE + JOIN queries (Q7 in bench_complex: ~4.5s) aren't affected by
either optimization — CTE materialization goes through a different
path that writes/reads a temp DBF. Follow-up target.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:47:20 +09:00
d2ed140273 feat(FiveSql2): SqlEach block callback — beats raw RDD on end-to-end timing
The structural 1.38x gap vs raw RDD for no-WHERE full scans wasn't
a limit of our engine — it was a limit of the result shape. SqlScan
materializes N rows as HbArray wrappers over a flat Value buffer,
then the PRG caller iterates that materialized array. Two passes
over the data. Raw RDD is one pass.

SqlEach folds both passes into one. The caller supplies a code block
that receives the selected column values as positional parameters;
SqlEach invokes it per matching row. No result array is ever built.

Usage (drop-in replacement for the common "scan + process" idiom):

    five_SQLEach( "SELECT id, name, salary FROM emp WHERE salary > 50000",
                  {|nID, cName, nSalary| Process(nID, cName, nSalary) } )

API shape borrows Harbour's AEval/ASort block-callback convention,
so there's nothing new to learn. Positional params also sidestep
the `SELECT COUNT(*)` naming problem — no need to invent names for
anonymous expressions.

Implementation notes:
  - 4-way loop specialization ({DBF, generic Area} × {WHERE, none}),
    matching SqlScan. Each path is zero-allocation in the steady state.
  - Block invocation uses the direct pendingParams + blk.Fn(t) protocol
    rather than EvalBlock, which would allocate a temporary args slice
    on every call (50k scans × small slice adds up).
  - FastFieldGetter is installed the same way as SqlScan so PcOpFieldGet
    in the WHERE predicate skips the PushSymbol + Function dispatch.

Bench (50k rows, end-to-end including user-code loop, steady state):

  Path                           Time     vs raw RDD
  ─────────────────────────────────────────────────────
  Raw PRG loop, WHERE + sum      8.7ms    1.00x
  SqlScan + PRG FOR, WHERE       5.1ms    0.59x
  SqlEach block, WHERE           4.1ms    0.47x  ← beats raw
  ─────────────────────────────────────────────────────
  Raw PRG loop, no WHERE         6.1ms    1.00x
  SqlEach block, no WHERE        3.8ms    0.62x  ← beats raw

SqlEach is faster than a hand-rolled `DO WHILE !Eof()` loop because
the per-row FieldGet in raw PRG still goes through a full Frame +
RTL dispatch, whereas SqlEach's FastFieldGetter captures the concrete
*dbf.DBFArea directly. The SQL abstraction now costs nothing — it
pays you to use it.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Next step (not in this commit): FiveSql2 TSqlExecutor integration —
detect when five_SQL is called with a block argument and route to
SqlEach instead of SqlScan + array build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:16:36 +09:00
b1d89b9783 perf(FiveSql2): PcOpFieldTrim fused peephole — string WHERE at raw RDD parity
Second pcode peephole to match the one added for FieldGet(literal).
SqlExprToPrg auto-wraps CHAR column references with AllTrim() to
match SqlCmpEq's CHAR-padding trim semantics, so every string WHERE
predicate evaluates `AllTrim(FieldGet(n)) == 'literal'` per row.

Before this commit each of those per-row evaluations did:
  1. PushSymbol ALLTRIM
  2. PushSymbol FIELDGET → Function(1)  [1 RTL Frame]
  3. parseCharField → MakeString       [alloc: copies raw bytes]
  4. Function(1) → AllTrim RTL         [1 RTL Frame]
  5. strings.TrimSpace                  [alloc: new string]
  6. Return, continue

New opcode `PcOpFieldTrim <idx>` (0x47) fuses the two RTL calls into
a single opcode that:
  1. Calls FastFieldGetter directly (no Frame/Function dispatch).
  2. Walks the returned string with ASCII-space trim in place.
  3. Pushes `s[lo:hi]` — a sub-slice, no new allocation.
  4. Short-circuits back to the same string if no trim needed.

genpc recognizes the shape `AllTrim(FieldGet(<int-literal>))` in
emitCall and emits the fused opcode automatically — no SQL-side
API change. Matches the existing FieldGet peephole's shape.

Bench impact (50k rows, 3-run steady state, vs raw RDD baseline 6.2ms):

  String WHERE          before 7.9ms → after 6.2ms   1.00x (parity!)
  Numeric WHERE         6.9ms (unchanged)            1.11x
  No WHERE              9.1ms (unchanged)            1.47x

String WHERE is now at parity with the raw Harbour-style RDD scan.
Compared to session start (119ms), that's a 19x speedup.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:03:03 +09:00
f9ffd4050e perf(FiveSql2): FieldGet peephole + DBFArea devirt — WHERE at ~1.15x raw RDD
Two stacked optimizations land on the SqlScan hot path. Combined
effect on the 50k-row benchmark:

                       Before    After   vs raw
  Numeric WHERE        10.2ms    7.8ms   1.15x
  String WHERE         10.5ms    7.9ms   1.15x
  No WHERE              9.2ms   10.0ms   1.45x
  Raw RDD baseline      6.8ms    6.8ms   1.00x

WHERE-predicate paths are now within 15% of the raw Harbour-style
RDD scan loop. The no-WHERE path is unchanged (slight jitter from
the added devirt branch); FieldGet peephole doesn't apply there.

--- Optimization 1: PcOpFieldGet peephole ---

Adds a new pcode opcode `PcOpFieldGet <fieldIdx>` (0x46) that skips
the usual PushSymbol+Function+Frame+FieldGet-RTL+EndProc chain and
calls a direct field getter closure instead. genpc recognizes the
shape `FieldGet(<int-literal>)` during emitCall and emits the
specialized opcode automatically — no SQL-side API change.

Integration:
  * hbrt.Thread.FastFieldGetter  — hot-path closure set by scan loops.
                                   Non-nil → pcode bypasses dispatch.
                                   Nil → pcode resolves FIELDGET via
                                   the RTL symbol table (correctness
                                   fallback for any other callers).
  * compiler/genpc/genpc.go      — peephole in emitCall.
  * hbrt/pcinterp.go             — PcOpFieldGet handler.

This alone cut numeric WHERE from 10.2 → 7.9ms: eliminated roughly
one full Frame/EndProc + RTL dispatch per row × 50k rows.

--- Optimization 2: DBFArea devirtualization ---

SqlScan type-asserts the workarea to *dbf.DBFArea once and runs a
dedicated loop that calls GoTop/EOF/Skip/GetValue directly on the
concrete type. Go's compiler inlines these, skipping the interface
vtable per row. Non-DBF drivers still work via the generic Area
branch.

The FastFieldGetter closure also captures *DBFArea directly in the
DBF branch, so the WHERE predicate side of the hot loop is now
entirely devirtualized: no interface dispatch between the pcode
dispatch loop and the DBF record buffer.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Remaining gap to raw RDD on no-WHERE (~1.45x) is dominated by the
two-column row construction + ArraySlab + flat backing bookkeeping
that the raw loop doesn't do. Going below that requires changing
the SQL engine's result shape — out of scope here.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:23:31 +09:00
b9296412af fix(gengo): INDEX ON ... TO (expr) evaluates filename at runtime
Prior behavior used exprToString() to serialize the TO expression
back into a string, so a runtime-evaluated filename like
`( Lower(cTable) + "_pk.ntx" )` ended up as the literal filename
`Lower(cTable) + "_pk.ntx"` on disk. Visible in FiveSql2's PRIMARY
KEY / UNIQUE DDL path: test_sql1999 was creating files with that
literal name, which the test happened not to care about because the
USE inside BEGIN SEQUENCE caught the failure.

Fix: if the File expression contains any function call (detected by
new containsCall walker), emit emitExpr + Pop2 + AsString — runtime
evaluation path. Static filenames (`TO test.ntx`) still use the
cheap exprToString branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:41:15 +09:00
3a00aa5435 feat(hbrtl): field metadata + index creation RTL — TSqlIndex warnings to zero
TSqlIndex.prg had five undefined identifiers and six undefined
constants that the new CLASS-method analyzer surfaced after the
gengo PushMemvar fallback stopped crashing on them. All real tech
debt, not false positives. This lands the implementations.

New RTL functions (hbrtl/indexrtl.go + register.go):
  - FieldType(n) → "C"/"N"/"L"/"D"/"M"/... one-letter type
  - FieldLen(n)  → length in bytes
  - FieldDec(n)  → decimal places
  - ordCreate(cBag, cTag, cExpr [, bExpr] [, lUnique])
      → DBFArea.OrderCreate with TagName set (CDX tag or NTX tag)
  - dbCreateIndex(cFile, cExpr [, bExpr] [, lUnique])
      → legacy Clipper single-tag NTX without TagName
  - dbClearIndex() → OrderListClear

All pass through the existing Indexer interface; key expressions go
through the MacroEval slow path since callers pass string literals.
When callers are updated to pass compiled key blocks, the existing
KeyFunc fast path kicks in automatically.

New header files (include/):
  - dbinfo.ch  — DBI_* and DBOI_* constants with Harbour-compatible
                 values (FULLPATH=10, SHARED=42, EXPRESSION=2, etc.)
  - dbstruct.ch — DBS_NAME/TYPE/LEN/DEC field descriptor indices

TSqlIndex.prg already did `#include "dbinfo.ch"` and `#include
"dbstruct.ch"` but Five's preprocessor silently ignored the missing
files. Both headers land in include/ where cmd/five's include-dir
chain already looks.

Analyzer RTL allow-list updated with the six new function names so
the warning pipeline stays clean.

Result: FiveSql2 build goes from 17 WARN → 0. Both tracked test
suites still pass.

Note: dbInfo() / dbOrderInfo() themselves remain stubbed (return NIL)
— the constants exist for compile-time resolution and for future use
when the stubs are replaced. Callers that depend on actual dbInfo
values still get NIL at runtime.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:11:57 +09:00
d89797c4e3 feat(analyzer): walk CLASS method bodies for undeclared-var warnings
Phase 2 of the analyzer originally only called analyzeFunc on
*ast.FuncDecl. Class methods parse as *ast.MethodDecl and were
silently skipped — meaning anything inside `METHOD Foo() CLASS TBar`
got zero static checking, including the undeclared-variable scan.

This is what let FindExclusive's DBI_FULLPATH / DBI_SHARED references
ship: the gengo fallback (now PushMemvar, previously PushLocal(0))
turned them into runtime NIL / crash, but the analyzer never flagged
them at build time because it never descended into the method body.

Fix: add analyzeMethod — same scope setup as analyzeFunc (module
statics, parameters, LOCAL/STATIC decls) — and route MethodDecl to
it from the Phase 2 dispatch.

Also register PCCOMPILE / PCEVAL / SQLSCAN in the RTL allow-list so
FiveSql2's new pcode hot-path RTL doesn't trip the warning.

Expected side effect: the FiveSql2 build now emits 17 real warnings
from TSqlIndex.prg — undefined DBOI_* order-info constants and
unregistered RTL functions (FieldType, FieldLen, ordCreate,
dbCreateIndex, dbClearIndex). These are real tech debt hiding behind
PushMemvar's silent NIL fallback; left as-is to surface them rather
than suppress.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./compiler/analyzer/... PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:46:33 +09:00
08ad6f4761 fix(gengo): unresolved identifiers fall back to PushMemvar, not PushLocal(0)
Three emitIdent / emitIdentByName / emitPopByName call sites used
`t.PushLocal(0)` as the fallback for compile-time-unresolved names
(missing #include constants, undeclared globals, typos). PushLocal(0)
crashes at runtime the moment that code path executes with "local
variable index out of range: 0" — even when the identifier is dead
code or behind a condition that's rarely true.

Concrete bugs this hid:
  - TSqlIndex:FindExclusive referenced DBI_FULLPATH / DBI_SHARED
    from a non-existent dbinfo.ch include. The 43-test harness only
    reached FindExclusive with no Used workareas, so the reference
    was never evaluated. Any standalone PRG that called five_SQL
    after dbUseArea would trip it.
  - Prior session's BindColumns/ResolveCache experiment hit the same
    class of crash in the CLASS Send path — diagnosed as "Unresolved
    → PushLocal(0)" at the time but root cause deferred.

Fix: use `t.PushMemvar(name)` / `t.PopMemvar(name)` instead. Matches
Harbour semantics (undefined identifiers try PRIVATE/PUBLIC memvar
tables at runtime, missing → NIL, assignment auto-creates PRIVATE).
Harbour is forgiving about unresolved names; Five now is too.

This doesn't silence the signal: the emitted comment still flags the
reference as unresolved for grep-ability in generated Go.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:20:26 +09:00
6b26f1b642 feat: genpc.CompileExpr + PcCompile/PcEval runtime bytecode API
Expose Five's existing FRB bytecode compiler for single-expression
compilation, enabling prepared-statement-style caching in dynamic
query engines (FiveSql2, scripting layers, rule engines).

1. genpc.CompileExpr(ast.Expr) *hbrt.PcodeFunc
   - New public API that compiles a single expression to a
     standalone pcode function
   - Reuses genpc's mature emitExpr (no new emit logic)
   - ExecPcode manages the frame around the generated code

2. hbrtl.PcCompile(cPrgExpr) -> pFunc
   - RTL entry point for runtime compilation
   - Wraps the expression in a FUNCTION stub, uses the full PRG
     parser pipeline (pp + parser + genpc), extracts the compiled
     pcode function, returns it as an opaque pointer
   - Callers pay parse+compile cost ONCE per expression

3. hbrtl.PcEval(pFunc) -> xValue
   - RTL entry point for runtime execution
   - Calls hbrt.ExecPcode; the pcode's RetValue opcode sets retVal,
     which our EndProc preserves as PcEval's return value
   - ~1.2x slower than direct FieldGet (pcode interpreter overhead),
     but eliminates AST tree-walk per row for complex expressions

Usage (FiveSql2 hot path, planned):
   pc := PcCompile("FieldGet(4) > 50000")  // parse+compile once
   WHILE !Eof()
      IF PcEval(pc)                         // ~10us per row
         AAdd(aRows, ...)
      ENDIF
      dbSkip()
   ENDDO

Benchmark (50k records, WHERE salary > 50000):
   Raw FieldGet:      7.9 ms  (baseline)
   FieldPos+Get:     10.2 ms  (with O(1) FieldPos cache)
   PcEval bytecode:  10.1 ms  (interpreted bytecode)
   MacroEval:        parse+eval per row — orders of magnitude slower

Tests:
   go test ./...        ALL PASS (14 packages)
   FiveSql2 43/43       100%
   compat_harbour       51/51
   PcCompile/PcEval     verified on 50k-row scan

FiveSql2 engine integration deferred — requires careful PRG-level
refactoring to thread pcode pointers through the plan structure.
The Go-level infrastructure is now in place for that work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:57:52 +09:00
ed33af41c5 perf: FieldPos O(1) cache + xbase import detection for function-call PRGs
Two SQLite-style optimizations for RDD and SQL workloads:

1. FieldPos() O(1) column binding cache

   Before: FieldPos(name) linear scan — O(n) per call with string
           comparison. In SQL engines that call FieldPos per row per
           column, this is hundreds of thousands of calls.

   After:  DBFArea builds a map[UPPER(name)]→pos on first lookup.
           All subsequent lookups are O(1) hash. SQLite calls this
           "column affinity binding" — positions resolved at prepare,
           not per row.

   Implementation:
     - hbrdd/dbf/dbf.go: DBFArea.FieldPosCache(name) method
     - hbrtl/procinfo.go: FieldPos RTL uses fieldPosCacher interface
     - Lazy init: only pays for tables that get queried

2. hbrdd import auto-detection for function-call style PRGs

   Before: compiler only added hbrdd import when PRG used xBase commands
           (USE, SKIP, INDEX...). Pure function-call style like
           `dbUseArea(.T.,,"t")`, `FieldPut(1, val)` was missed —
           generated Go failed to compile ("undefined: hbrdd").

   After:  scanStmtsForXBase walks ExprStmt bodies too, detecting
           CallExpr to any of the ~40 xBase RTL function names.
           FIELD->NAME alias expressions also trigger the import.

   Resolves: small PRGs that use only dbUseArea/FieldGet/FieldPut.

Benchmark notes (50k records):
  Raw RDD scan:              7 ms    (baseline)
  FiveSql2 SELECT WHERE:   157 ms    (unchanged — bottleneck is
                                      not FieldPos, it's PRG-level
                                      expression tree walk per row)
  compat_harbour 51/51:    PASS
  FiveSql2 43/43:          100%

The FieldPos cache helps heavy field-name-based code paths but the
primary FiveSql2 bottleneck is the PRG interpreter walking expression
ASTs per row (needs bytecode compilation to close the gap).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:42:00 +09:00
7cc729f394 perf(index): compiled key evaluator — UDF INDEX 2.7x faster
Eliminate MacroEval overhead for INDEX ON with UDF/complex expressions.

Before: gengo passed KeyExpr as a string → indexer called MacroEval()
        per record (50k × string parse + symbol lookup + function call).

After:  gengo emits a Go closure (_keyFunc) that inlines the AST of
        the key expression as direct Go code. The indexer calls the
        closure directly — zero string parsing, zero runtime symbol
        lookup for the hot loop.

Three code paths in the closure, depending on expression type:
  1. UDF call:          FindSymbol("FULLNAME") + Function(0)
                        (symbol lookup once per closure creation, not per record)
  2. Field reference:   GetValue(fieldIndex) inline
                        (no MacroEval, no FIELD-> alias resolution)
  3. UPPER/LOWER(expr): strings.ToUpper/Lower inline
                        (no RTL function call overhead)

Architecture (Go compiler design principle):
  Compile time knows the AST → emit native code.
  Don't serialize to string → re-parse at runtime 50k times.

Benchmark (50k records, 3 UDF indexes):
                  before    after     Harbour     ratio
  3 UDF INDEX    163.0ms   60.0ms    55.0ms      Five/HB = 1.09x
  SEEK 10k         7.6ms    7.6ms    14.0ms      Five 1.8x faster
  SCAN 50k         3.4ms    3.4ms     4.0ms      Five 15% faster
  TOTAL          233.0ms  130.0ms   147.0ms      Five 12% faster overall

UDF INDEX build went from 3x SLOWER than Harbour to nearly EQUAL.
SEEK/SCAN remain faster than Harbour (mmap + NTX optimizations).

Changes:
  hbrdd/driver.go     KeyFunc field in OrderCreateParams
  hbrdd/dbf/indexer.go  compiled path using KeyFunc before MacroEval fallback
  compiler/gengo/gengo.go  emitIndexKeyExpr: field-aware AST→Go emitter
                           for INDEX ON key expressions

Correctness: Harbour vs Five UDF diff = 0 (25-line output match)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:36:37 +09:00
4d5621c21a feat: CDX compound index write + {||} parsing + zero known constraints
All 3 remaining known constraints resolved. CLAUDE.md now shows zero.

1. CDX compound index WRITE support (was read-only)

   New file: hbrdd/cdx/build.go (~400 LOC)
   - CreateOrAddTag() builds Harbour-compatible CDX files
   - Bit-packed leaf pages (RecBits/DupBits/TrlBits compression)
   - Interior nodes with big-endian RecNo/ChildPage
   - Compound root directory (structural B-tree of tag names)
   - Append-safe: preserves existing tags when adding new ones
   - Linked leaf pages (LeftPtr/RightPtr for sequential scan)

   Pipeline: INDEX ON expr TAG tagname TO file
   - ast.IndexCmd gains TagName field
   - Parser captures TAG name (was discarded)
   - gengo passes TagName to OrderCreateParams
   - indexer.go routes to cdx.CreateOrAddTag when TAG specified

   Verified: 3 tags (BYNAME/BYCITY/BYAGE), OrdSetFocus by name,
   SEEK, GoTop/GoBottom, close+reopen with SET INDEX TO

2. {||} empty code block parsing in function arguments

   Parser's parseArrayOrBlock() called parseExpr() unconditionally
   after closing |, failing when body was empty ({||}).
   Fix: check for RBRACE after closing | and emit NIL literal body.
   {=>} empty hash already worked.

3. Semicolon IF...ENDIF — already worked (removed from constraints)

Tests:
  go test ./...        14 packages ALL PASS
  FiveSql2             43/43 100%
  compat_harbour       51/51

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:58:09 +09:00
5bfdc476ef fix: STATIC inside FUNCTION — persistent variables now work
Before: `STATIC n := 0` inside a FUNCTION caused "local variable
index out of range: 0" panic. The gengo code generator only handled
module-level STATIC (file scope) but silently ignored function-level
STATIC declarations.

After: Function-level STATIC variables are emitted as Go package-level
vars with function-name prefixed names (e.g., `static_COUNTER_N`),
registered in staticVars map during function emission, and cleaned up
after the function to prevent name collisions.

Also fixes compound assignment (+=, -=, *=, /=) on STATIC variables,
which previously only handled simple assignment (:=).

   FUNCTION Counter()
      STATIC n := 0    // persists across calls
      n++              // n++ already worked (postfix handler)
      n += 10          // was broken, now works
   RETURN n

Verified:
  Counter() → 1, 2, 3           (n++)
  CountA() → 10, 20, 30         (n += 10, separate scope)
  CountB() → 101, 102, 103      (n += 1, init 100, separate scope)

  go test ./...        14 packages OK
  FiveSql2             43/43 100%
  compat_harbour       51/51

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:49:33 +09:00
3adc9d7d59 fix: PCount, Break/RECOVER, SET INDEX TO — 3 Harbour compat fixes
Release-blocking compatibility issues discovered during the 258-test
pre-release validation suite (100 syntax + 44 RDD + 114 RTL).

1. PCount() always returned 0 in PRG code

   Root cause: ParamCount() returned t.pendingParams, which is
   overwritten by every nested Function() call. By the time the
   PCount() RTL's Frame() executes, pendingParams is already 0.

   Fix: Frame() now stores pendingParams in frame.paramCount.
   PCount() RTL uses CallerParamCount() which reads callSP-2
   (the PRG caller's frame), while RTL functions still use
   ParamCount() (reads pendingParams before their own Frame).

   Verified: PCount(1,2,3)=3, PCount(1)=1, PCount()=0

2. Break("string") panicked instead of being caught by RECOVER USING

   Root cause: Generated SEQUENCE code only caught *HbError panics.
   Break() panics with BreakValue (a different type), which fell
   through to EndProc's "runtime error" message and re-panic.

   Fix (two parts):
   a) gengo emitBeginSequence: recover closure now catches any
      panic (interface{}), then dispatches via type switch:
      - *HbError → extract .Error() string
      - hasValue interface (BreakValue) → extract .GetValue()
      - other → static "error" string
   b) hbrtl/error.go: BreakValue gets GetValue() method for
      duck-type detection without import cycles
   c) hbrt/thread.go EndProc: BreakValue type name check added
      so it re-panics silently (no stderr noise)

3. SET INDEX TO a, b, c only opened the last file

   Root cause: Parser's parseSet() called parseExpr() once for
   INDEX setting, stopping at the first comma. Remaining file
   names were consumed by the "eat rest of line" loop.

   Fix: Parser now collects comma-separated identifiers into a
   single string literal "a,b,c". gengo splits on comma and
   calls OrderListAdd() for each file.

   Verified: SET INDEX TO si_name, si_city → OrdCount=2

All tests pass:
  go test ./...          14 packages OK
  FiveSql2               43/43  100%
  compat_harbour         51/51
  Syntax test           100/100
  RDD test               44/44
  RTL test              114/114
  Windows cross-compile  OK
  Linux cross-compile    OK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:06:28 +09:00
fc1dca9551 feat(rdd): real POSIX file/record locking + gap analysis doc
Replaces the FLOCK/DBRLOCK/DBRUNLOCK no-op stubs with actual
fcntl(F_SETLK) byte-range advisory locks, matching Harbour's
hb_fsLockLarge implementation.

Before: rtlDbRLock always returned .T. regardless of contention.
        Multi-process writers could silently corrupt records.

After:  Non-blocking POSIX byte-range locks per file descriptor.
        Cross-process exclusion verified by a subprocess-spawning
        Go test that witnesses BUSY vs OK transitions.

New files:
  hbrdd/dbf/locks_posix.go    fcntl F_WRLCK/F_UNLCK wrappers
  hbrdd/dbf/locks_windows.go  stub (TODO: LockFileEx)
  hbrdd/dbf/lock_multi_test.go   cross-process verification
  docs/gap-analysis.md        honest Harbour parity assessment

Modified:
  hbrdd/dbf/dbf.go
    - DBFArea gains fileLocked bool + lockedRecs map
    - Close() calls releaseAllLocks() before dropping the fd
  hbrtl/database.go
    - rtlDbRLock / rtlDbRUnlock now delegate to DBFArea.LockRecord /
      UnlockRecord instead of returning fixed .T./NIL
    - New rtlFLock / rtlDbUnlock for FLOCK() / DBUNLOCK()
  hbrtl/register.go
    - FLOCK and DBUNLOCK symbols registered (were missing entirely)
  compiler/analyzer/analyzer.go
    - FLOCK / DBUNLOCK added to RTL known-function set

Lock region layout (non-overlapping on purpose):
  FLOCK region       [0, HeaderLen+1)
  Record N region    [RecordOffset(N), RecordLen)

So a workarea can hold FLOCK and multiple DBRLOCK simultaneously
on the same fd without conflict.

Design rationale (captured in locks_posix.go header):
  * POSIX fcntl, not flock(2) — byte-range + NFS-safe
  * Non-blocking F_SETLK — matches Clipper FLOCK() → .F. semantics
  * Released explicitly on Close to avoid workarea-sharing races
  * Windows falls back to no-op (TODO: LockFileEx)

Verification:
  go test ./hbrdd/dbf/ -run TestFLockBlocksAcrossProcesses  PASS
  go test ./hbrdd/dbf/ -run TestRLockBlocksAcrossProcesses  PASS
  go test ./...                                             ALL PASS
  FiveSql2 43/43                                            100%
  compat_harbour 51/51                                      100%

The gap-analysis doc (docs/gap-analysis.md) is a running inventory
of what works vs what's still missing vs Harbour 3.2, written for
users evaluating Five for production — not a sales pitch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:58:03 +09:00
e95afad4ee feat: Harbour RDD parity — NTX/CDX 100% compatible, FIELD-> works
Five RDD engine now matches Harbour DBFNTX and DBFCDX byte-for-byte
in ordering, seek, navigation, and field access. Verified against
Harbour 3.2.0dev with a 281-line comparison test covering:
  - Natural/NAME/CITY/AGE/SALARY/UPPER ordering
  - SEEK (exact/not-found), GoTop/GoBottom per order
  - DELETE/RECALL with SET DELETED
  - CDX compound index read with 5 tags (BYNAME, BYCITY, BYAGE, BYSAL, BYUNAME)
  - Reverse traversal

Fixes:

1. FIELD->NAME returned NIL
   GetAliasField returned interface{} but runtime expected hbrt.Value,
   so the type assertion in PushAliasField failed and pushed NIL.
   - workarea.go: change return type to hbrt.Value, handle FIELD/_FIELD
     as current-workarea alias, add SetAliasField
   - gengo.go: emit SetAliasField() for alias->field := value in both
     statement and expression contexts

2. OrdSetFocus(n) silently switched to natural order
   v.AsString() returns "" for a numeric Value, so OrderListFocus("")
   set current=-1.
   - indexrtl.go: convert numeric param via fmt.Sprintf("%d", ...)

3. CDX compound tag order mismatched Harbour
   Five decoded the structural B-tree which is alphabetical, but
   Harbour sorts tags by TagBlock (file offset = creation order).
   - cdx/cdx.go: sort tagEntries by offset ascending after decoding,
     matching hb_cdxIndexLoadAvailTags in dbfcdx1.c

4. OutStd()/OutErr() not registered — caused panic on call
   - hbrtl/console.go: add rtlOutStd/rtlOutErr implementations
   - hbrtl/register.go: register OUTSTD and OUTERR
   - analyzer.go: add OUTSTD/OUTERR to RTL known-functions

5. FIELD keyword triggered "undeclared variable" warnings
   - analyzer.go: add FIELD, _FIELD, M, MEMVAR as builtin constants

Tests:
  go test ./...        — ALL PASS (17 packages)
  FiveSql2 43/43       — 100%
  compat_harbour 51/51 — 100%
  Harbour diff         — 0 lines differ (281-line comparison)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:37:47 +09:00
02026a1966 fix: analyzer zero warnings — complete RTL coverage, cross-file awareness
- Register all 479 RTL functions from hbrtl/register.go (was ~60)
- Recognize module-level STATIC variables across all functions
- Declare RECOVER USING variables in analyzer scope
- Register code block parameters ({|x,y| ...}) as declared
- 2-pass multi-file build: collect cross-file function names before analysis
- Add QUIT, ERRORLEVEL, ALTSRC to builtin constants

All 3 test suites pass with 0 warnings:
  go test ./...        — ALL PASS
  FiveSql2 43/43       — 100%
  compat_harbour 51/51 — 100%

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:11:08 +09:00
486e466592 feat: FiveSql2 43/43, @byref, mutable closure, RTL 479, DateTime fix
Major changes since last commit:
- FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS
- 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.)
- @byref pass-by-reference via RefCell pattern
- Mutable closure capture (EnsureLocalRef + RefCell sharing)
- RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8)
- DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display)
- Reserved word guard (39 keywords blocked from function calls)
- AEval arg order fix (element before index)
- Closure capture redecl fix (unique _cap_ names per block)
- Hash/string indexing in ArrayPush/ArrayPop
- Harbour compat test suite: 51/51
- 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:35:37 +09:00
d451b836a6 perf: inline Str/PadR/PadL/SubStr/Left/Right/At/IIF in gengo
13 more RTL functions inlined — no Frame/EndProc, no VM dispatch:
- Str(n,w,d) → fmt.Sprintf("%*.*f", w, d, n)
- PadR(s,n) → s + hbrtl.Spaces(n-len(s))
- PadL(s,n[,fill]) → Spaces(pad) + s or Repeat(fill, pad) + s
- SubStr(s,p,l) → s[p:p+l] with bounds check
- Left(s,n) → s[:n], Right(s,n) → s[len-n:]
- At(search,target) → strings.Index + 1
- IIF(cond,a,b) → if/else without function call

Also: Spaces() exported for generated code access.

50K SEEK random: 62ms (Harbour 67ms — Five FASTER!)
82/82 stress PASS. 14 packages ALL PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:16:38 +09:00
197720f869 fix: Go code review — 7 critical issues resolved
From senior Go developer review:

C7 CRITICAL: pagePool data race (ntx.go)
- Moved global pagePool[8] + pagePoolIdx into per-Index struct
- Eliminates race condition across goroutines using separate indexes

C8 CRITICAL: Page.data dangling pointer after remap (ntx.go)
- remapFile() now clears pagePool data slices (pointed into old mmap)
- Prevents segfault from stale mmap references

C4 HIGH: pop() bounds check restored (thread.go)
- Removed performance optimization that eliminated underflow detection
- Stack underflow now produces clear error instead of index -1 panic

C1 HIGH: intExpLen overflow on MinInt64 (value.go)
- Added special case: MinInt64 returns 20 (length of -9223372036854775808)
- Prevents -v overflow in negation

C11 CRITICAL: GoTo ReadAt error handling (dbf.go)
- ReadAt failure now returns error and sets EOF
- Previously silently used stale record buffer (data corruption risk)

C14 HIGH: LEN() inline missing Hash case (gengo.go)
- Added _v.IsHash() → len(Keys) branch

C15 HIGH: EMPTY() inline missing Date case (gengo.go)
- Added _v.IsDate() && _v.AsJulian() == 0 check

82/82 stress PASS. 14 packages ALL PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:26:34 +09:00
44d3c7385c perf: fused opcodes + inline EOF/BOF/Found/RecNo/Deleted
Fused opcodes (ops_compare.go):
- LocalLessEqualInt: FOR i<=N without Push+LessEqual+PopLogical
- LocalGreaterEqualInt: FOR STEP -1
- Direct local access + int comparison (no stack, no Value boxing)

gengo FOR loop:
- Detects literal TO value → emits LocalLessEqualInt (3 calls → 1)
- Falls back to stack-based for variable limits

Inline RDD functions (gengo tryEmitInlineRTL):
- EOF/BOF/Found/Deleted/RecNo/RecCount: direct area method call
- No FindSymbol + PushNil + Do(0) + Frame/EndProc overhead
- Uses hoisted _darea when inside DO WHILE context

Results (50K, ext4):
  SEEK random: 63ms (Harbour 67ms — FASTER!)
  SEEK seq: 44ms (Harbour 27ms — 1.6x)
  CDX SEEK NAME: 47ms (Harbour 27ms — 1.7x)
  CDX SEEK ID: 24ms (Harbour 17ms — 1.4x)

All counts correct. 82/82 stress PASS. 14 packages ALL PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:07:34 +09:00
ad1bc23e36 perf: inline RTL + symbol cache infrastructure + EndProcFast
gengo inline RTL (tryEmitInlineRTL):
- LTrim/RTrim/AllTrim/Upper/Lower/Len/Empty/Chr/Asc
- Skip Frame/EndProc/VM dispatch entirely
- Emit direct Go code (strings.TrimLeft, etc.)

Symbol cache infrastructure (collectSymbols):
- AST walker collects all referenced symbol names
- symCache field ready for future per-function hoisting
- Currently disabled (function-level hoisting caused side effects)

NTX TestGetMmap helper for profiling.

82/82 stress PASS. 14 packages ALL PASS.
50K SEEK random: 64-66ms (Harbour 67ms — equal or faster)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:01:24 +09:00
77562d4645 perf: inline RTL functions in gengo — skip Frame/EndProc entirely
tryEmitInlineRTL: recognized RTL functions emit direct Go code
instead of PushSymbol → PushNil → Push args → Function(n) dispatch.

Inlined functions (most common in SEEK key generation):
- LTrim → strings.TrimLeft(s, " ")
- RTrim/Trim → strings.TrimRight(s, " ")
- AllTrim → strings.TrimSpace(s)
- Upper → strings.ToUpper(s)
- Lower → strings.ToLower(s)
- Len → len(s) / len(arr.Items)
- Empty → nil/zero/empty check
- Chr → string(byte(n))
- Asc → int(s[0])

Each inlined call saves: FindSymbol + PushNil + Frame + locals copy
+ function body + EndProcFast + return handling = ~0.14ms per call.

In 50K SEEK loop with 5 string functions: saves ~35ms.

50K SEEK random: 64ms (Harbour 67ms — Five FASTER!)
82/82 stress PASS. 14 packages ALL PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:50:25 +09:00
48cd4f9e5c perf: DO WHILE/SEEK/DELETE WA hoisting — reduce per-iteration lookups
DO WHILE optimization:
- Detect RDD commands in body (SKIP/GO/SEEK/REPLACE/DELETE)
- If no USE/SELECT (safe), hoist _dwa/_darea before loop
- SKIP/GO/SEEK/DELETE inside loop use cached area variable
- Eliminates WA lookup + Current() per iteration

SEEK optimization:
- Use hoisted area when inside DO WHILE or FOR hoist context
- Eliminates WA lookup per SEEK call in tight loops

DELETE optimization:
- Use hoisted area when available

All commands now check g.hoistedDW || g.hoistedFields:
- GO TOP/BOTTOM/n → cached area
- SKIP n → cached area
- SEEK key → cached area + Indexer check
- DELETE → cached area
- APPEND → cached area (FOR loop)
- REPLACE → cached _rdbf + _rfiN (FOR loop)

82/82 stress PASS. 14 packages ALL PASS.
CDX SCOPE: 12ms (Harbour 4ms = 3x)
NTX SCAN: 24ms (Harbour 5ms = 4.8x)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:25:38 +09:00
bb6cf7c612 perf: FOR loop RDD hoisting — WA/FieldIndex cached outside loop
When FOR body contains APPEND+REPLACE and no USE/SELECT:
- Hoist WorkAreaManager, Current(), *dbf.DBFArea outside loop
- Pre-compute FieldIndex for all REPLACE fields once
- REPLACE inside loop uses cached _rdbf and _rfiN variables
- APPEND inside loop uses cached _rarea (no WA lookup per iter)

Safety: collectReplaceFields returns nil if USE/SELECT found in body
(workarea may change → cannot safely cache). Falls back to normal emit.

10K APPEND benchmark: 28ms (Harbour 27ms — essentially equal!)
82/82 stress test PASS. 14 packages ALL PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:07:33 +09:00
8f354ae24d perf: gengo RDD optimization — reduce VM overhead per operation
REPLACE (gen_cmd.go):
- Cache area.(*dbf.DBFArea) once per command (was N times for N fields)
- Remove _fi >= 0 check (FieldIndex returns -1, PutValue handles it)
- Reduces type assertions from N+1 to 2

GO/SKIP (gen_cmd.go):
- Literal integer parameters emit directly (no Push/Pop stack ops)
- GO 5 → _area.GoTo(uint32(5)) instead of Push(5) → Pop → AsNumInt
- SKIP -1 → _area.Skip(-1) instead of Push(1) → Negate → Pop

Benchmark impact (50K, ext4):
- SEEK random: 138ms → 121ms (12% improvement)
- DUPKEY scan: 41ms → 37ms (10% improvement)
- DELSCAN: 32ms → 28ms (13% improvement)

82/82 stress test PASS. 14 packages ALL PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:39:11 +09:00
dadb97ee88 fix: 3-level NTX correctness + CDX SET INDEX TO string quoting
NTX 3-level tree (build.go):
- Hybrid approach: bulk build for ≤2 levels, insertKeyBTree for 3+
- rebuildWithInsert: creates proper B-tree via per-key insertion
- 5000-key test: Count=5000 Found=5000 (was 5004/4868)

CDX SET INDEX TO (gengo.go):
- Strip surrounding quotes from string literal in OrderListAdd
- Was: idx.OrderListAdd("\"path\"") → file not found
- Now: idx.OrderListAdd("path") → correct

All tests:
- 14 packages ALL PASS
- 82/82 NTX stress test
- 18/18 CDX cross-read
- 50K benchmark: all counts correct

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:04:07 +09:00
adede5cd69 perf: REPLACE remove Flush + bulk build + deferred write = 1600x faster
Critical fix: REPLACE was calling area.Flush() after every field write!
- gengo gen_cmd.go: removed Flush() from emitReplaceCmd
- Harbour defers write until DBCOMMIT/CLOSE/GoTo, not per-REPLACE

Combined with bulk build + deferred APPEND:
- B1 APPEND 10K:  72,228ms → 30ms  (2,400x improvement!)
- B2 INDEX NAME:  34ms → 5ms       (6.8x improvement)
- Harbour comparison: Five 30ms vs Harbour 27ms (1.1x)

Also: OrderCreate flushes dirty record + EOF + header before index build

Benchmark on ext4 (home dir):
┌─────────────┬──────────┬────────┬───────┐
│ Benchmark   │ Harbour  │ Five   │ Ratio │
├─────────────┼──────────┼────────┼───────┤
│ APPEND 10K  │ 27ms     │ 30ms   │ 1.1x  │
│ INDEX NAME  │ 2ms      │ 5ms    │ 2.5x  │
│ INDEX CITY  │ 0ms      │ 7ms    │ -     │
│ SEEK 10K    │ 6ms      │ 25ms   │ 4.2x  │
│ SCAN FWD    │ 1ms      │ 6ms    │ 6x    │
│ SCAN BWD    │ 0ms      │ 6ms    │ -     │
│ PACK        │ 4ms      │ 3ms    │ 0.75x │
└─────────────┴──────────┴────────┴───────┘

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:22:05 +09:00
b7028791d6 fix: 5 seek/dbf bugs — 77/77 thorough Harbour compatibility
1. SOFTSEEK: use idx.CurRecNo() for positioning (was checking recNo > 0)
   - SEEK with SET SOFTSEEK ON now positions at next higher key
   - SEEK command reads SET SOFTSEEK at runtime (was compile-time only)
   - rtlDbSeek defaults to GetSetSoftSeek() when no explicit param

2. SET DELETED ON + INDEX: SkipIndexed skips deleted records
   - GoTopIndexed: skip deleted record at top position
   - SkipIndexed: inner loop continues past deleted records

3. Compound key (CITY+NAME): field name TrimSpace before lookup
   - evalKeyExprInner: TrimSpace on fieldName after FIELD-> strip
   - Fixed "CITY " != "CITY" mismatch from + operator splitting

4. SET INDEX TO filename: treated as string, not variable
   - gengo uses exprToString for SET INDEX TO (was emitExpr)
   - Prevents identifier being resolved as local variable

5. hasXBaseCommands: recursive scan into nested blocks
   - BEGIN SEQUENCE, IF, FOR, DO WHILE, SWITCH bodies now scanned
   - Fixes missing hbrdd import for DB commands inside blocks

Thorough test: 77 items (14 sections) covering exact/partial/soft seek,
SET DELETED, duplicate keys, numeric keys, compound keys, empty/single
table, state consistency, order switching, full traversal — all identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:08:51 +09:00
c04c9aeaa8 feat: INDEX ON with UDF support — user functions in key expressions
Core change:
- dbf.KeyEvalFunc: global callback set by gengo before OrderCreate
- evalKeyExprInner default case: calls KeyEvalFunc for unknown functions
- Final fallback: any unresolvable expression → KeyEvalFunc → MacroEval
- valueToKeyBytes: converts MacroEval result to index key bytes
- gengo: sets dbf.KeyEvalFunc = t.MacroEval before OrderCreate, clears after

Examples that now work:
  INDEX ON MyFunc(FIELD->NAME) TO idx    // UDF in key expression
  INDEX ON CityKey(FIELD->CITY, NAME) TO idx  // multi-param UDF
  INDEX ON Left(MyFunc(NAME), 15) TO idx // nested built-in + UDF

Also fixed:
- SET ORDER TO n: int→string via hbrt.NtoS (was empty string)
- CDX compound leaf decoder: proper bit-packed tag name extraction
- CDX compound recNo = direct byte offset (not page number)

All existing tests pass, NTX 47/47 + CDX 20/20 Harbour compat maintained.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:36:21 +09:00
7e2a159b88 feat: CDX support + ORDSCOPE + cross-read Harbour compatibility
CDX Integration:
- IndexEngine interface: common for NTX Index and CDX Tag
- OrderListAdd: auto-detects .cdx/.ntx extension, opens CDX tags
- decodeCompoundLeaf: proper bit-packed tag directory decoding
  (was stub falling through to scanCompoundLeaves with wrong names)
- CDX Tag: added KeyLen(), KeyExpr(), ForExpr(), IsDescending(), Close()
- CDX compound recNo = direct byte offset (not page number)

ORDSCOPE:
- SetScope/ClearScope/SetScopeTop/SetScopeBottom on DBFArea
- GoTopIndexed: seeks to scopeTop, validates within scopeBottom
- GoBottomIndexed: seeks to scopeBottom boundary
- SkipIndexed: stops at scope boundaries (top and bottom)
- OrdScope RTL function registered (nScope: 0=TOP, 1=BOTTOM)
- scopeKeyFromValue: converts Value to padded key bytes

Index Order Management:
- OrderListFocus: handles numeric order ("2" → order 2)
- SET ORDER TO n: gengo emits hbrt.NtoS for int-to-string conversion
- IndexOrd/OrdCount/OrdName/OrdKey: real implementations (were stubs)
- OrderCount/CurrentOrder/OrderName/OrderKeyExpr accessors on DBFArea
- ClearScope on order switch (prevents stale scope)

Cross-read test: Harbour-created CDX → Five reads, 20/20 items match:
  NAME/CITY/ID seek, ORDSCOPE count, GoTop/GoBottom all identical

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:21:26 +09:00
6e78d12cc2 fix: 3 RDD compat bugs — FIELD->, AsNumInt Double, PACK/ZAP with index
Bug 1: FIELD->NAME in INDEX ON expression
- evalKeyExprInner: strip FIELD->/alias-> prefix before field lookup
- exprToString: handle AliasExpr (FIELD->NAME → "FIELD->NAME")

Bug 2: AsNumInt() on Double returned IEEE 754 raw bits
- Value.AsNumInt(): check tDouble and convert via Float64frombits
- Fixed array index crash when index is result of % modulo

Bug 3: PACK/ZAP crash with open indexes
- OrderListRebuild: fully implemented (was TODO stub)
  Saves index info, closes all, sets idxState=nil, recreates
- OrderCreate: set current=-1 during key evaluation (natural GoTo)
- PACK/ZAP: save/restore idxState, rebuild after operation
- Register __DBPACK, __DBZAP, DBRECALL symbol aliases

Harbour vs Five: 45/47 match (96%), 2 diffs are duplicate-key sort order

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 04:41:19 +09:00
21fd9dc65c feat: SET DELETED filtering, SEEK/LOCATE/CONTINUE, SET command codegen
- skipFilter: skip deleted records in GoTop/GoBottom/Skip when SET DELETED ON
- hbrdd.IsSetDeleted callback: avoids circular import hbrdd→hbrtl
- Parser: capture ON/OFF for boolean SET commands (DELETED, EXACT, SOFTSEEK, etc.)
- Parser: capture TO expr for SET DATE/DECIMALS/EPOCH
- Gengo: emit proper t.Do() calls for 11 SET toggles + 3 value SETs
- stmtSet: was stub (skipToEOL), now calls parseSet()
- RTL: register 11 SET toggle functions (SETDELETED, SETEXACT, etc.)
- RTL: DBLOCATE/DBCONTINUE for sequential search
- RTL: DBSETFILTER/DBCLEARFILTER/DBFILTER
- PadL/PadR: support 3rd param fill character
- Area interface: added SetFound, SetLocate, LocateBlock, filter methods
- MemRDD: implements new Area interface methods
- Comprehensive PRG test: test_search.prg (7 test suites all pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:33:59 +09:00
48a471bb1d fix: Phase 5 — MEDIUM #27,30,31 + LOW #25,41 complete cleanup
Files modified (6):
  compiler/parser/parser.go — #27: Add currentUpper() helper
    Replaces 30 strings.ToUpper(p.current.Literal) calls
  compiler/parser/stmtreg.go — Remove now-unused strings import
  compiler/parser/expr.go — #30: Document comma expr Harbour semantics
  compiler/gengo/gengo.go — #31: Replace 8 TODO comments with WARN
    Macro expr now emits MacroPush() instead of TODO
  compiler/token/token.go — #25: Replace itoa with strconv.Itoa
    #41: Add 50+ missing kindNames entries for complete String()

Issues resolved: #25,27,30,31,41
Total fixed: 39/53

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:00:44 +09:00
6ffcf77dd8 fix: Phase 3 — #25,28,29,41 token/AST/parser cleanup
Files modified (4):
  compiler/token/token.go — #25: Replace hand-rolled itoa with strconv.Itoa
    Fixes math.MinInt overflow bug in original implementation
  compiler/ast/ast.go — #29: Fix VarDecl.End() returning last var position
    Was returning Pos() (useless span info)
  compiler/parser/stmtreg.go — #28: Eliminate all 7 token array mutations
    rewriteAsIdent() modifies p.current only, not the token array
    Prevents backtracking corruption and improves safety
  compiler/lexer/lexer.go — Already clean from Phase 2

Issues resolved: #25 (MEDIUM), #28 (MEDIUM), #29 (MEDIUM), #41 partial (LOW)
Total fixed: 29/53

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:58:20 +09:00
f950cb0784 fix: Phase 2 — HIGH #6,9,10,11,12,19,23,32,46,47
Files modified (5):
  compiler/gengo/gengo.go — #6,#32: Deduplicate 3 Generate functions into 1
    doGenerate(file, debug, library) replaces 170 lines of copy-paste
    Dead GenerateDebug method removed
  cmd/five/main.go — #9,10,11,12: Fix 5 silently ignored errors
    filepath.Abs, tidy.Run, tidyCmd.CombinedOutput now checked
  hbrtl/strings.go — #19: Str() now reads caller's nWidth/nDec params
    Was ignoring explicit Str(123, 10, 2) arguments
  compiler/pp/pp.go — #46: Fix stale "NOT implemented" comment
    #47: Extract maxIncludeDepth constant
  compiler/lexer/lexer.go — #23: Remove unused LookupKeyword result

Issues resolved: 10 (HIGH: 7, MEDIUM: 3)
Total fixed: 26/53

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:47:26 +09:00
d7513eeb24 fix: Code review round 2 — race conditions, dead code, hardcoded paths
CRITICAL fixes:
- #1 vm.go: Mutex on libModules/dynamicFuncs global slices
  RegisterLibModule/RegisterDynamicFunc now thread-safe
  RegisterLibModules copies under lock, clears, releases
- #4 shutdown.go: Signal handler goroutine leak fixed
  Uses done channel + select for clean exit on normal shutdown

HIGH fixes:
- #7-8 gobridge.go: Remove dead if/else branches (both identical)
- #13-14 main.go: Remove hardcoded /mnt/d/harbour-core paths
  Use HB_INC env var + standard /usr/local/include/harbour only
- #15 main.go: Remove unused frbModSeq variable

MEDIUM fixes:
- #22 expr.go: Remove unused parts variable in parseInterpolatedString
- #51 macro.go: Remove var _ = fmt.Sprintf import guard
- macroeval.go: Remove unused lexer import and guard

Total fixed this session: 12/53 issues resolved
Remaining: 41 (CRITICAL: 1, HIGH: 9, MEDIUM: 16, LOW: 16)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:32:09 +09:00
0828d17159 feat: Harbour RTL vs Go math comparison example + analyzer IMPORT fix
- examples/go_math_compare.prg: Side-by-side comparison of
  Harbour RTL (Abs, Sqrt, Round, Int, Max, Min, Log, Exp, Mod)
  vs Go math package (Sin, Cos, Pow, Pi, Floor, Ceil, Hypot, ...)
- Combined usage: normal distribution, compound interest, distance
- Analyzer: recognize IMPORT package names as valid identifiers
- Analyzer: add math RTL functions (ABS, SQRT, etc.) to known list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:35:40 +09:00
59568f3301 Five v0.9 — Harbour + Go fusion language
- 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>
2026-03-31 09:41:50 +09:00