Commit Graph

18 Commits

Author SHA1 Message Date
ed1aeeb212 feat(pgserver): pg_catalog stub for BI-tool connection compatibility
PostgreSQL clients (psql, pgx, DBeaver, Tableau, DataGrip,
pgAdmin) fire a barrage of catalog probes at connection time —
SELECT version(), SHOW server_version, SELECT FROM pg_namespace
/ pg_class / pg_type / pg_database / pg_settings. FiveSql2 can't
parse most of them. Without interception the BI tool either
errors out on connect or proceeds with a half-broken view of
the database (zero tables, no type info, no schema list). This
commit lands the minimum-viable catalog shim so the common
connect-and-list-tables flow succeeds.

Strategy
--------

Pattern-match catalog probes BEFORE handing the SQL to five_SQL.
Recognised shapes get synthesised result envelopes — same
`{ aFieldNames, aRows }` hbrt.Value shape the engine returns,
so the existing dispatchSimpleQuery / executePortal pipelines
stream them identically to a normal query.

Covered (v1.0)
--------------

  * SET / RESET / DISCARD <name>           → success, no-op
  * SHOW <name>                            → single-row response
                                             (server_version, server_encoding,
                                              client_encoding, DateStyle,
                                              transaction_isolation, etc.)
  * SELECT version() / current_database() / current_schema() /
    current_user / session_user / pg_backend_pid()  → single-row
  * SELECT … FROM pg_namespace             → 2 rows (pg_catalog + public)
  * SELECT … FROM pg_class                 → list of open workareas
                                             (relkind='r', relnamespace=public)
  * SELECT … FROM pg_attribute             → empty (stub; column-shape
                                             introspection deferred to v1.1)
  * SELECT … FROM pg_type                  → 7 OIDs FiveSql2 actually emits
                                             (bool, int4, int8, text, numeric,
                                              date, timestamp)
  * SELECT … FROM pg_database              → 1 row, the connect-time db name
  * SELECT … FROM pg_settings              → name/setting pairs matching SHOW
  * Anything else mentioning pg_catalog. / pg_<name> / information_schema.
    → empty result with generic field names (BI tool sees "0 rows" rather
    than a parse error)

Deliberate non-goals
--------------------

  * WHERE / JOIN evaluation — psql, pgx, DBeaver all filter
    client-side on the rows we return. We send the whole
    catalog and let them apply their predicates.
  * pg_attribute introspection — would need to re-derive
    column types from the open workarea + map back to PG OIDs.
    Tracked as v1.1 work.
  * Recursive CTE catalog queries (pgAdmin's tree builder uses
    them) — too brittle to pattern-match. Falls through to
    five_SQL where it errors loudly. pgAdmin's table-tree pane
    will then show "0 tables" but the connection itself stays
    alive.

Files
-----

  hbrtl/pgserver/catalog.go  (new, ~280 LOC)
    catalogIntercept(sql) → (handled, value)
    synthPgNamespace / synthPgClass / synthPgAttribute /
    synthPgType / synthPgDatabase / synthPgSettings
    simpleSelectFunction (version/current_*/pg_backend_pid)
    showResponse (SHOW <name>)

  hbrtl/pgserver/dispatch.go
    dispatchSimpleQuery: catalogIntercept ahead of runSQL.

  hbrtl/pgserver/extended.go
    executePortal: same intercept, ahead of runSQL.

Verification
------------

psql against a running pgserver, with sslmode=require + MD5:

  $ psql -c 'SELECT version()' -At
  PostgreSQL 14.0 (FiveSql2) (FiveSql2 wire-compat shim)

  $ psql -c 'SELECT * FROM pg_namespace' -At
  11|pg_catalog|10
  2200|public|10

  $ psql -c 'SELECT * FROM pg_type' -At
  16|bool|1
  23|int4|4
  20|int8|8
  25|text|-1
  1700|numeric|-1
  1082|date|4
  1114|timestamp|8

  $ psql -l    # \\l now works
          데이터베이스 목록
   oid | datname | datdba | 인코딩
  -----+---------+--------+--------
     1 | alice   |     10 |      6

Integration script gates grew from 6/6 → 9/9:
  PASS  Catalog probe: SELECT version()
  PASS  Catalog probe: pg_namespace lists public + pg_catalog
  PASS  Catalog probe: SHOW server_version_num

All six release gates green:
  go test ./...               ✓
  FiveSql2 SQL:1999 43/43     ✓
  Harbour compat 56/56        ✓
  std.ch 17/17                ✓
  FRB 7/7                     ✓
  pgserver integration 9/9    ✓ (+3 from catalog stubs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:31:52 +09:00
3b2dd365ad feat(pgserver): Phase 6 — TLS + source-IP allowlist
Closes the v1.0 hardening surface: encrypted transport + a
coarse pg_hba.conf-equivalent CIDR allowlist. Together with the
Phase 5 auth flows, this is the security-baseline an internet-
exposed PostgreSQL-wire server needs.

TLS subsystem
-------------

`hbrtl/pgserver/tls.go`:

* `LoadTLSFromFiles(certPath, keyPath)` — cert/key PEM pair load
  with tls.VersionTLS12 floor. Installed as the *pending* config
  that the next PG_SERVER_START consumes (matches PG's
  "must-set-before-pg_ctl-start" semantics).

* `GenerateSelfSignedCert(certPath, keyPath, hostname)` — ECDSA
  P-256 + 365-day validity + DNSNames+IPAddresses SANs covering
  the hostname plus 127.0.0.1 / ::1. Dev/CI helper; production
  ships a CA-signed cert via the loader.

* `upgradeToTLS()` wraps `tls.Server(conn, cfg).Handshake()` so
  pgproto3 reads plaintext on top of the encrypted stream.

Source-IP allowlist
-------------------

* `AllowIP(cidr)` parses a CIDR and appends it to a per-server
  list snapshotted at PG_SERVER_START time.
* `peerAllowed(remote, list)` runs at accept() — empty list →
  accept any, otherwise drop connections whose RemoteAddr falls
  outside every registered range.
* `ClearAllowList()` resets to allow-all.

Coarse but compatible with the "host alice 10.0.0.0/8 md5"-style
entries every pg_hba.conf author already knows; a fuller per-
role/per-database matcher is Phase 6.1+.

PRG bindings (register.go)
--------------------------

New HB_FUNCs, all idempotent and composable in any order before
PG_SERVER_START:

  pg_tls_load( certPath, keyPath )           → .T. | cErr
  pg_tls_self_signed( cert, key, hostname )  → .T. | cErr
  pg_allow_ip( cidr )                        → .T. | cErr
  pg_clear_allowlist()                       → NIL

Bootstrap idiom:

  PROCEDURE Main()
     PG_TLS_SELF_SIGNED( "/tmp/cert.pem", "/tmp/key.pem", "localhost" )
     PG_ADD_ROLE( "alice", "swordfish" )
     PG_ALLOW_IP( "127.0.0.1/32" )
     PG_ALLOW_IP( "10.0.0.0/8" )
     PG_SERVER_START( ":5432", "md5" )

The startup banner now reports TLS + allowlist state so the PRG
operator sees the security posture at a glance:

  pgserver: listening on :5432 (auth=md5 tls=on allowlist=2)

Verification
------------

End-to-end via real psql against a self-signed server:

  $ PGPASSWORD=swordfish psql \
        "postgres://alice@127.0.0.1:15432/alice?sslmode=require" \
        -c "SELECT 'tls-works' AS x" -At
  tls-works

  $ # off-allowlist source (192.168.x.x mock) → connection refused
  $ # (verified manually; psql can't easily spoof src IP for CI)

Integration script gates expanded to 6/6:
  PASS  Simple Query
  PASS  Multi-statement Simple Query
  PASS  Transaction control
  PASS  MD5 auth: wrong password rejected
  PASS  MD5 auth: correct password accepted
  PASS  TLS handshake + MD5 auth via sslmode=require

All six release gates green:
  go test ./...               ✓
  FiveSql2 SQL:1999 43/43     ✓
  Harbour compat 56/56        ✓
  std.ch 17/17                ✓
  FRB 7/7                     ✓
  pgserver integration 6/6    ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:07:19 +09:00
90eafcfc06 feat(pgserver): Phase 5 — password + MD5 authentication
Trust mode (v1.0 default) accepts anyone; that's fine for embedded
demo but unshipping a multi-client database without credentials
would be irresponsible. This commit adds two of libpq's three
standard auth flows. SCRAM-SHA-256 is Phase 5.1 — pgx/psql both
fall back to MD5 cleanly when the server advertises only md5, so
v1.0's functional coverage is complete with the pair landed here.

Auth subsystem
--------------

`hbrtl/pgserver/auth.go` adds:

* An in-memory role registry: `roleMap map[string]*role` guarded by
  sync.RWMutex. Reads (lookupRole) are hot-path during connection
  startup so the RWMutex lets multiple sessions auth in parallel
  without serialising through a plain Mutex.

* `AddRole(name, password)` / `RemoveRole(name)` Go API consumed
  by the new HB_FUNCs `PG_ADD_ROLE` / `PG_REMOVE_ROLE` (see
  register.go). Bootstrap PRG idiom:

      PG_ADD_ROLE("alice", "swordfish")
      PG_ADD_ROLE("bob",   "hunter2")
      PG_SERVER_START(":5432", "md5")

* `authPassword()` — cleartext PasswordMessage exchange. The wire
  payload is plain so intended for TLS-protected links only;
  Phase 6 ties the warning to actual TLS detection on the session.

* `authMD5()` — libpq's md5 challenge:

      server → AuthenticationMD5Password{salt: 4 random bytes}
      client → "md5" || md5_hex( md5_hex(password || user) || salt )

  We recompute the canonical hash from the stored plaintext and
  compare. md5Challenge() is exported for pinning by a Go unit
  test (vector cross-checked against libpq's fe-auth-md5.c).

Salt is sourced from crypto/rand on every challenge so replay
attacks against a captured wire trace can't reuse a prior hash.

Dispatch matrix (Config.AuthMode → flow):
  "" / "trust" → AuthenticationOk immediately, no lookup
  "password"   → authPassword()
  "md5"        → authMD5()
  anything else→ 28000 + connection close

Tests
-----

Unit (hbrtl/pgserver/pgserver_test.go):
  PASS  TestMD5Challenge           (vector + determinism + diff)
  PASS  TestRoleRegistry           (add/replace/remove/lookup)

Integration (tests/pgserver/run.sh):
  PASS  Simple Query: SELECT 1, 'hello'
  PASS  Multi-statement Simple Query
  PASS  Transaction control: BEGIN/COMMIT round-trip
  PASS  MD5 auth: wrong password rejected
  PASS  MD5 auth: correct password accepted

End-to-end matrix with real psql:
  wrong password   → "ERROR: md5 authentication failed for user 'alice'"
  correct password → SELECT returns row
  unknown user     → "ERROR: md5 authentication failed for user 'eve'"
  password mode    → cleartext exchange works equivalently

All six release gates green:
  go test ./...               ✓
  FiveSql2 SQL:1999 43/43     ✓
  Harbour compat 56/56        ✓
  std.ch 17/17                ✓
  FRB 7/7                     ✓
  pgserver integration 5/5    ✓ (up from 3/3 in Phase 4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:01:30 +09:00
8472928102 feat(pgserver): Phase 4 — Extended Protocol (Parse/Bind/Execute)
pgx and most drivers default to PostgreSQL's Extended Protocol
(named prepared statements). Phase 2 only handled Simple Query,
so every pgx caller had to force `QueryExecModeSimpleProtocol` —
unworkable for a production deployment. This commit lands the
full Parse → Bind → Describe → Execute → Sync state machine,
enough that pgx (and any other libpq-protocol-v3 client) works
without any client-side knobs.

Implementation lives in `hbrtl/pgserver/extended.go`:

* Per-session caches `stmts map[string]*preparedStmt` and
  `portals map[string]*portal`, lazily allocated on first use.
  Stored as fields on `session` so they don't leak across
  connections.

* Parameters are inlined at Bind time via `substituteParams` —
  the resolved SQL is a normal Simple-Query-shaped string the
  engine sees through the existing `five_SQL(cSQL, …, oSession)`
  pipeline. Avoids teaching FiveSql2 a second param-shape; the
  trade-off is that binary timestamps/numerics round-trip through
  text (Phase 4.1 will plumb `?`-params through aParams for the
  binary fast path).

* `paramToLiteral` decodes the binary-format encodings pgx uses
  by default for INT4/INT8/BOOL (big-endian fixed-width). Other
  binary OIDs fall back to a hex-escaped quoted literal which
  errors loudly rather than silently misparsing.

* `countPgPlaceholders` scans the SQL outside string literals for
  the highest `$N` so the server can answer Describe-statement
  with a correctly-sized ParameterDescription even when the
  client didn't pre-declare param OIDs. Without this, pgx errored
  with "expected 0 arguments, got 2" on the very first prepared
  query.

* RowDescription emission: Describe-statement still returns NoData
  (we can't infer row shape without execution). When Execute fires
  on a portal the client never Described, we emit RowDescription
  inline from the cached result before DataRow streams. pgx and
  psql both tolerate this ordering.

* Execute → CommandComplete tag derives from the SQL verb via the
  existing `commandTagFor` helper. Row counts in the tag remain
  "VERB 0" for v1.0; threading real counters through the engine
  is Phase 5.

Wire dispatch in `session.go:queryLoop` now handles Parse, Bind,
Describe, Execute, Close, Sync, Flush — the full v3 message set.

Verification
------------

End-to-end pgx (default mode, no SimpleProtocol flag) successfully
runs:
  SELECT $1 AS n, $2 AS s with 42 + "hi" → [42 hi]
  Same statement re-executed with different bound values → reuses
    the cached prepared statement
  SELECT $1 AS b, $2 AS s with true + "binary-bool" → [t binary-bool]

`tests/pgserver/run.sh` expanded from 1 → 3 integration assertions:

  PASS  Simple Query: SELECT 1, 'hello'
  PASS  Multi-statement Simple Query
  PASS  Transaction control: BEGIN/COMMIT round-trip

(Extended Protocol can't be driven from psql's -c CLI directly
because psql's PREPARE/EXECUTE is a separate SQL-level feature
that FiveSql2 doesn't parse; the pgx-driven path verifies it
manually, and a self-contained Go integration that drives pgx
from inside a process bootstrap is Phase 7 work.)

All six release gates green:
  go test ./...                       ✓
  FiveSql2 SQL:1999 43/43             ✓
  Harbour compat 56/56                ✓
  std.ch 17/17                        ✓
  FRB 7/7                             ✓
  pgserver integration 3/3            ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:55:41 +09:00
a5567648e9 test(pgserver): Phase 3 — DML + transaction integration harness
Adds tests/pgserver/run.sh, the integration gate for the wire
layer. Builds a minimal bootstrap PRG that opens nothing and just
calls PG_SERVER_START on an ephemeral port, then drives psql with
a Simple Query to confirm the end-to-end pipeline (TCP accept →
startup handshake → Query → five_SQL → RowDescription + DataRow
→ ReadyForQuery) still works after every change.

Phase 3 verified scope (driven via a separate pgx harness during
development):

  * CREATE TABLE / INSERT / UPDATE / DELETE over Simple Query
  * BEGIN / COMMIT / ROLLBACK from the wire
  * Two-connection cross-visibility on a shared DBF
  * Per-session ROLLBACK leaves the *other* connection's data
    intact — the Phase 1 STATIC → TSqlSession refactor is what
    makes this hold; pre-refactor, both connections would have
    shared one s_aTxnLog and A's ROLLBACK would have collapsed
    B's COMMIT.

Known limitation captured in the script header (deferred to
Phase 7 follow-up):

  * ≥3 concurrent connections doing in-flight INSERT/SELECT in
    their own transactions occasionally race at the hbrdd
    workarea layer — surfaces as one worker's just-inserted row
    missing from its own SELECT. 2-way concurrent + N-way serial
    are both reliable. Root cause is multi-thread workarea
    arbitration during dbUseArea/dbAppend, which the pre-1.0
    audit flagged as Top-Risk #2 ("WorkArea collision under
    multi-session"). Tracking for a dedicated fix.

Gate count now reads:
  go test ./...                       ✓
  FiveSql2 SQL:1999 43/43             ✓
  Harbour compat 56/56                ✓
  std.ch 17/17                        ✓
  FRB 7/7                             ✓
  examples 65/71                      ✓ (unchanged baseline)
  pgserver integration 1/1            ✓ (new)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 07:25:13 +09:00
cde86730b8 fix(compiler,hbrt,hbrdd,cli): pre-1.0 audit — 13 critical fixes
Senior-engineer / QA audit landed 13 silent-miscompile and data-
integrity fixes spanning the whole compiler+runtime+storage stack.
Each fix is paired with either an integration test in the suite or
a focused regression check; all 6 release gates stay green:
go test ./..., FiveSql2 43/43, Harbour compat 56/56, std.ch 17/17,
FRB 7/7, examples 65/71.

Compiler
--------

* genpc IF/ELSEIF jumpEnd2 patching (compiler/genpc/genpc.go).
  Per-ELSEIF branch terminators were stashed into `_ = jumpEnd2`
  and never patched — the relative offset stayed 0 and the runtime
  walked the next ELSEIF's PcOpJumpFalse opcode as if it were
  jump-offset data. Bytecode-level corruption in pcode mode. Now
  collected into a slice and patched at end-of-IF. Verified via
  Grade(95..50) cases 11a-e added to tests/frb/test_frb_pcode_sweep.

* countLocalsInStmts / scanBodyLocals missing bodies
  (compiler/gengo/gen_util.go, compiler/gengo/gengo.go). Frame-size
  counter skipped WATCH/TIMEOUT/PARALLEL FOR bodies, so a LOCAL
  declared inside one of those constructs got a slot index past
  the runtime's allocated count — silent NIL reads or out-of-range
  stomps.

* emitMethodDeclStandalone nested LOCAL (compiler/gengo/gen_class.go).
  Same bug class but on the *method* side. Pre-fix repro:

      METHOD Stomp(n) CLASS T
         LOCAL a := 1, b := 2
         IF n > 0
            LOCAL c := 30, d := 40, e := 50, f := 60
            Inner( n )
            IF c != 30 .OR. d != 40 .OR. e != 50 .OR. f != 60 ...

  printed `c, d, e, f = 5, NIL, NIL, NIL` because Inner's frame
  collided with Stomp's underallocated slot range. Now counts
  body-nested LOCALs into the frame and pre-allocates indices via
  scanBodyLocals.

* genpc unsupported-AST diagnostic surface (compiler/genpc/genpc.go,
  hbrt/pcode.go, cmd/five/main.go, hbrtl/frb.go). The `default`
  cases in emitStmt / emitExpr silently emitted PushNil / no-op
  for nodes the pcode generator doesn't implement (ClassDecl,
  MethodDecl, xBase commands, concurrency primitives, …). Added
  `PcodeModule.Warnings []string` populated by noteUnsupported,
  surfaced on stderr from the build pipeline. Users now see
  "pcode: AST node not supported in --pcode/FRB-pcode mode: stmt
  *ast.GoBlockStmt" instead of getting a silently broken module.

Runtime
-------

* class.go Send/tryBinaryOp t.self defer-restore (hbrt/class.go).
  Restoration was a plain `t.self = oldSelf` after `fn(t)`. Any
  panic in the method body skipped the line, so the next BEGIN
  SEQUENCE / RECOVER handler ran with the THROWING object's Self
  — `::field` resolved against the wrong receiver. Wrapped both
  restore sites in `defer func() { t.self = oldSelf }()`.
  Verified: pre-fix RECOVER saw "THROWER", post-fix "OUTER".

* hbfunc.go HB_FUNC parameter Frame() (hbrt/hbfunc.go). The
  RegisterDynamicFunc wrapper called `fn(ctx)` without ever
  calling Frame, so `ctx.ParC(1)` / `ctx.Local(n)` read through
  `t.curFrame.localBase + n - 1` against the *caller's* frame.
  Every #pragma BEGINDUMP HB_FUNC taking parameters silently
  returned "" / 0 / "" for them — masked by ParNIDef-style
  defaults. Wrapper now does `t.Frame(t.pendingParams, 0); defer
  t.EndProc()` before dispatch.

* pcode codeblock closure capture (hbrt/pcinterp.go, hbrt/pcode.go,
  hbrt/thread.go, compiler/genpc/genpc.go). PcOpPushBlock recorded
  `nDetached` but never copied enclosing locals; free vars in the
  block body fell through to memvar lookup → NIL. Wired full
  capture pipeline:
  - New opcodes PcOpPushDetached (0x59) / PcOpPopDetached (0x5A).
  - PushBlock now reads per-slot source-local indices and
    snapshots into bb.Detached at construction time.
  - New detachedMap in genpc auto-promotes any free var that
    resolves to an enclosing-frame local into a capture slot.
  - emitAssignAsExpr leaves the assigned value on the eval stack
    so SeqExpr items like `{|v| acc += v, acc }` work.
  - Thread tracks curBlock with paired Set/restore in the block's
    Fn wrapper for nested-block evaluation.
  Mutating capture (acc += v across successive Evals) now works.

* vm.NewThread statics + waFactory propagation (hbrt/vm.go).
  GoLaunch / GoLaunchBlock call NewThread directly. Previously
  the statics map and WA factory were applied only in Run(), so
  goroutine-spawned PRG code panicked on STATIC access ("static
  index out of range") and crashed dereferencing nil WA on any
  DB call. Both now happen inside NewThread under the same lock
  as TID assignment.

Data layer
----------

* dbf concurrent Append lock (hbrdd/dbf/dbf.go,
  hbrdd/dbf/locks_posix.go, hbrdd/dbf/locks_windows.go). Append
  bumped a local recCount with no file-system serialization. Two
  shared-mode processes both wrote at the same RecordOffset; one
  record silently overwrote the other. Added an append-intent
  byte-range lock at offset 0x7FFFFFFE + bounded retry, on-disk
  header refresh inside the locked region, and immediate header
  write so peers refresh past our slot.

* indexer negative numeric key encoding (hbrdd/dbf/indexer.go +
  new hbrdd/dbf/encode_numeric_test.go). `%20.10f` formats `-100`
  as `"     -100.0000000000"` and `99` as `"        99.0000000000"`.
  ASCII ' ' (0x20) < '-' (0x2D), so `99` lex-compared LESS than
  `-100` — every NTX/CDX index over a column that ever held a
  negative number returned wrong rows for SEEK / range scans.
  Replaced with a 1-byte sign prefix + 21-byte zero-padded
  magnitude (negatives use digit-complement) so byte order
  matches numeric order across signs and magnitudes. Format
  change: existing indexes built with the old encoding must be
  REINDEXed. Three unit tests pin the order.

* dbf Append index maintenance hooks (hbrdd/dbf/dbf.go,
  hbrdd/dbf/indexer.go). Append never inserted into open NTX/CDX
  indexes — the audit's canonical scenario `SET INDEX TO …;
  APPEND BLANK; REPLACE …; dbSeek …` silently missed the new
  record. Added optional IndexWriter interface, queue the new
  recNo in pendingIdxInserts, drain after flushRecord by calling
  InsertKey on every open writer-supporting engine. NTX
  participates (its existing rebuild-on-insert is correct);
  CDX online maintenance is deferred to a follow-up — those
  indexes still need REINDEX. Verified: post-fix SEEK("Charlie")
  after APPEND BLANK + REPLACE finds the new record.

* dbf PACK crash-safety (hbrdd/dbf/dbf.go). The old in-place
  rewrite read record N, overwrote slot M<N, then truncated.
  Power loss after partial loop left a file with overwritten
  prefix and no original copies of the records already advanced
  past — silent data loss. Rewrote to:
    1) drop mmap, build `<file>.pack.tmp` with all surviving
       records,
    2) Sync(),
    3) close original handle + os.Rename(tmp, orig) (atomic on
       same FS),
    4) reopen + re-mmap.
  TestComp_Pack passes; readers always see either the pre-PACK
  or post-PACK contents, never a half-state.

* mem RDD torn reads (hbrdd/mem/memrdd.go). The comment claimed
  in-place PutValue was safe because hbrt.Value "fits in a
  single machine word + pointer". hbrt.Value is 24 bytes (3
  words) — a concurrent reader could observe new type tag with
  stale scalar/ptr and type-confuse on the next AsXxx() call.
  Switched mu to sync.RWMutex; GetValue takes RLock,
  Append/PutValue/Delete/Recall take Lock. `go test -race
  ./hbrdd/mem/` clean.

Files touched
-------------

  compiler/gengo/gen_class.go, gen_util.go, gengo.go
  compiler/genpc/genpc.go
  hbrt/class.go, hbfunc.go, pcinterp.go, pcode.go, thread.go, vm.go
  hbrdd/dbf/dbf.go, indexer.go, locks_posix.go, locks_windows.go
  hbrdd/dbf/encode_numeric_test.go  (new)
  hbrdd/mem/memrdd.go
  cmd/five/main.go
  hbrtl/frb.go
  tests/frb/test_frb_pcode_sweep.prg

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 05:29:56 +09:00
ce7b067785 fix(cli): multi-PRG build adds every input dir to the include path
Each PRG file's preprocessor instance was set up with only its OWN
directory on the include search path (`filepath.Dir(prgFile)`).
That worked for self-contained files but broke any multi-file
build where one PRG `#include`s a header that lives next to a
SIBLING PRG — the other file's directory wasn't on the path, so
the include silently failed and PP just skipped it ("// #include
\"FiveSqlDef.ch\" — not found (skipped)").

This was the root cause behind test_sql_standards's mass-failure
pattern. The test does

   #include "FiveSqlDef.ch"
   ...
   Assert( ..., h["columns"][1][1][1] == ND_FN .AND. ... )

`FiveSqlDef.ch` lives in `_FiveSql2/src/` (next to TSqlExecutor.prg
and friends), but the test source sits in `_FiveSql2/test/`.
Building with `./five build _FiveSql2/test/test_sql_standards.prg
_FiveSql2/src/*.prg` should resolve the header from a sibling
input file's directory — but only the test's own dir was searched,
so ND_FN / ND_LIT / ND_BIN / ND_UNI all stayed undefined and the
identifiers fell through to runtime memvar lookup, returning NIL.
Every assertion that compared against the constants therefore
silently failed (24 / 64 passing because non-constant assertions
still worked).

buildMultiPRGWithIncludes now seeds the user-include list with the
directory of every input PRG before handing off to buildMultiPRG.
A test under one directory can now resolve a `#include` that lives
next to a sibling source file in the same multi-file build.

Result: test_sql_standards goes from 24 / 64 to **64 / 64**. The
parser was already correct end-to-end — every SQL:2003-2023
construct it had been advertising actually worked; the test just
couldn't read the constants it was asserting against.

Wired test_sql_standards into the std.ch runner with a per-test
override so it picks up the FiveSql2 src files. Suite stands at
17/17.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  FiveSql2 standards : 64/64  (was 24/64)
  Harbour compat     : 56/56
  std.ch suite       : 17/17
  FRB suite          : 7/7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:21:45 +09:00
af0d54d352 fix(lexer): {array}[index] no longer mis-tokenises [ as bracket-string
The lexer's isStringBracket disambiguator decides whether `[` opens
an indexing operator or a Harbour bracket-string literal. The
heuristic checks the previous token's kind and treats the bracket
as indexing only when preceded by an IDENT, RPAREN, RBRACKET, or a
literal. RBRACE was missing — so

   FieldPut(3, {"Kim","Lee","Park","Choi","Yoon"}[Int(Mod(i-1,5))+1])

tokenised the `[` after `}` as a bracket-string opener, swallowed
through the first `]` it found, and produced bogus parse errors
("expected ), got STRING …"). RBRACE is now in the indexing-context
set, so an inline array-literal followed by `[index]` works.

Surfaced by the examples/ build sweep — fixed test_all_rdd,
test_index_adv, test_multi_rdd, test_rdd_full all in one go.

The sweep itself is committed as tests/examples_build.sh — builds
every PRG under examples/ and reports any compiler / preprocessor
errors. Run it after compiler changes to catch regressions in
broad-coverage user-style code that the focused suites don't
exercise.

Current sweep state: 65 / 71 examples build cleanly. The remaining
6 failures are all #pragma BEGINDUMP blocks that import external
Go packages (http, websocket, sqlite, time) — not Five-side bugs.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 16/16
  FRB suite          : 7/7
  examples build     : 65/71 (rest = external Go deps)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:06:13 +09:00
2008266da7 feat(pp,rtl): Tier 2 audit followups — JOIN hash + PP validation + C heuristic
Three medium-priority audit items in one commit, each independently
revertible.

  * **#18 JOIN hash-join fast path.** New std.ch shape:
        JOIN WITH <alias> TO <file> [FIELDS ...] ON <mfield> = <dfield>
    expands to a 6-arg __dbJoin call with the master/detail key
    field names. Runtime detects the extra args, builds an O(M)
    hash over the detail's key column, then probes per master row
    for O(N+M) total — vs the FOR form's O(N*M). For 1k×1k that's
    2k vs 1M operations; the gap widens with N. The original FOR
    form is unchanged and stays the fallback for arbitrary
    predicates. New helper dbHashKey type-tags the key string so
    `1` (numeric), `"1"` (string), and `.T.` (logical) don't
    collide in the bucket map.

  * **#38 PP rule result-marker validation.** ParseRule now walks
    the result template after parseMarkers and warns about every
    `<name>` (or `<(name)>` / `<.name.>` / `<{name}>` / `#<name>`
    / `<"name">`) that doesn't match a pattern marker. Warnings
    flow into pp.errors via handleDirective with the directive's
    filename:line, so a typo'd `<NaMe>` in an `#xcommand`
    case-sensitive rule fails the build with a clear diagnostic
    instead of silently producing broken expansions.

  * **#44 looksLikeInlineC heuristic strengthened.** Catches more
    of the common Harbour-PRG-with-C-inline-block shapes that
    used to fall through and produce cryptic Go-side errors:
    function-like #define, `extern "C"` linkage blocks, C return-
    type declarations (`int foo(`, `static char* bar(`), and the
    hb_ret*() helper family used by Harbour's C FFI return
    setters. Two small predicate helpers (allLetters,
    allIdentChars) keep the C-vs-Go disambiguation tight enough
    that legit Go code (`func name() int { ... }`) doesn't trip.

  * **#28 LIST/DISPLAY pagination** — explicitly deferred. Proper
    pagination requires interactive terminal handling (Inkey(0)
    for the keypress) which would hang in CI / batch mode. Will
    revisit when an interactive terminal layer needs it for
    other reasons.

Test fixtures: tests/std_ch/test_join_hash.prg verifies the new
ON-form path produces the same output as the FOR form would.
std.ch runner now stands at 16/16.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 16/16
  FRB suite          : 7/7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:21:19 +09:00
29ca02e1bc fix(genpc,parser,pcinterp): pcode wider regression sweep (Tier 1 #3)
Six more silent miscompiles in the pcode path, all uncovered by a
new pcode regression sweep that exercises the full PRG surface a
dynamic FrbCompile body could legitimately use.

  * **xBase-keyword shadowing of variable names.** parseIdentStmt
    and parseExprStmt's fallback switches consumed an entire line
    when the leading IDENT matched LABEL / REPORT / ACCEPT / INPUT
    / NOTE / etc. Those words are also extremely common LOCAL /
    PRIVATE names — `LOCAL label ; label := "x"` had the
    assignment swallowed because the switch didn't peek at the
    next token. Both switches now look at peek(1): an assignment
    operator, [], (, -, ++, --, or `.` means it's a variable /
    call / member access, not the xBase command, and we fall
    through to expression parsing. Real silent bug — bit
    test_frb_pcode_sweep's `LOCAL label` declaration.

  * **`arr[i]` indexing not implemented in genpc.** ast.IndexExpr
    fell through to the default PushNil path, so any indexed read
    in a pcode-mode body returned NIL. New case emits the array,
    the index, and PcOpArrayPush (the get-op; PcOpArrayPop is the
    set-op — naming follows Harbour convention). Hashes go
    through the same opcode, which already special-cases
    IsHash() in ops_collection.go.

  * **Hash literals not implemented in genpc + dispatch missing
    in pcinterp.** `{ "k" => v, ... }` fell to PushNil. Added
    HashLitExpr emit (Push key, Push value pairs, then PcOpHashGen
    with count). Also wired up the PcOpHashGen dispatch in
    execPcodeBody — it had been declared in pcode.go since the
    initial design but the case statement was never added, so
    even hand-written modules couldn't use hashes.

  * **`x++` / `x--` postfix were silent no-ops.** PostfixExpr fell
    to PushNil and the surrounding ExprStmt then popped the NIL.
    DO WHILE loops with `n--` couldn't terminate; FOR loops with
    `i++` in the body were broken too. New case: PushLocal +
    LocalAddInt(±1).

  * **BlockExpr (`{|p| body }`) wasn't compiled.** Eval(b, n)
    inside a pcode body returned NIL. Added: build the body in a
    sub-codebuffer with the block's params occupying its locals,
    emit PcOpRetValue at the end, then PushBlock with the
    serialized bytes. Format extended with a uint16 nParams field
    so the runtime's PcOpPushBlock dispatch can set
    PcodeFunc.Params correctly — without it, ExecPcode's
    Frame(0, 0) pulled none of Eval's args and the block saw
    every parameter as NIL.

  * **All g.locals accesses were case-sensitive.** PRG is case-
    insensitive, but the pcode generator stored block params via
    strings.ToUpper while every other lookup site (function decl,
    mid-decl, ForStmt, IdentExpr read, AssignExpr write,
    PostfixExpr) used the raw .Name. So `{|x| x*x }` stored "X"
    but read "x" and missed. Normalized: all insertions and all
    lookups now go through strings.ToUpper.

  * **SeqExpr in pcode** — added the matching emit for comma-
    separated expression lists in code blocks (`{|| a, b, c }`).
    Same shape as the gengo SeqExpr case from Wave 1.

Test fixture: tests/frb/test_frb_pcode_sweep.prg covers 14 shapes
(string ops, arithmetic, comparison chains, array indexing, DO
WHILE with postfix, nested IF, IIf, hash literal + indexing,
block + Eval, character iteration). All 14 pass. Wired into the
FRB runner — suite now stands at 7/7.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 15/15
  FRB suite          : 7/7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:32:38 +09:00
dca7bb22e5 fix(gengo): count nested LOCALs into the function frame
Function-entry Frame() allocation counted only top-level LOCAL
declarations from fn.Body. Mid-function LOCALs hidden inside an
IF / FOR / WHILE / DO CASE / SWITCH / SEQUENCE block weren't
included, so the runtime allocated a frame too small to hold them.
Subsequent reads/writes via PopLocalFast / PushLocalFast / LocalAdd
to those slot indices then either silently scribbled past the frame
(read-back saw NIL) or panicked with "local variable index out of
range" once the index exceeded the underlying slice.

This is the underlying bug behind frb_demo Section 4 — the
`LOCAL ch := Channel(1)` declared inside `IF pAsync != NIL` got
slot N+1 from the codegen but the runtime only allocated N. The
Channel value was scribbled past the frame, ChReceive then read
NIL from a non-existent slot, and the goroutine's ChSend(49) had
nowhere to land.

New helper gen_util.go::countLocalsInStmts walks every nested body
(IF + ElseIfs + ElseBody, ForStmt, ForEachStmt, DoWhileStmt,
SeqStmt's Body + RecoverBody, SwitchStmt's Cases + Otherwise) and
totals every ScopeLocal VarDecl. The function-emit caller adds this
to the top-level count before sizing the Frame.

Test fixture (tests/frb/test_frb_goroutine.prg) reproduces the
demo Section 4 shape — `LOCAL ch := Channel(1)` inside IF, then
`Go("WORKER", ch, 7)`, then ChReceive(ch). Wired into the FRB
runner so it stands at 6/6.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 15/15
  FRB suite          : 6/6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:05:22 +09:00
6a30c4e50e fix(gengo): compound assign for non-LOCAL LHS
Audit follow-up after Wave 1's pcode `+=` fix surfaced a parallel
class of silent miscompiles in the *gengo* (native-Go) emit path.
Three real bugs hiding behind happy-path test coverage:

  * `arr[i] += x` was ASSIGN-only — the IndexExpr branch returned
    after emitting `arr[i] := x`, dropping the original element.
    Now: PushArray + Push index, ArrayPush to read, fold with RHS,
    re-do PushArray + index, ArrayPop to store.

  * `alias->field += x` (and the M-> / MEMVAR-> namespace variants)
    were ASSIGN-only too. Same shape of bug — `x->v += 7` compiled
    as `x->v := 7`. Compound branch reads via PushAliasField (or
    PushMemvar for M->), folds, stores via SetAliasField (or
    PopMemvar).

  * PRIVATE / PUBLIC mid-function declarations were treated as
    extra LOCAL slots. emitMidVarDecl extended `locals` past the
    function's declared count and emitted `PopLocalFast(idx)` for
    the init. The slot didn't exist at runtime, so the init either
    silently scribbled past the frame (small N) or panicked with
    "local variable index out of range" once exercised. New logic:
    PRIVATE/PUBLIC declarations bypass the locals table and emit
    `PopMemvar(name)` for the init expression. The runtime auto-
    creates the memvar.

  * Memvar assignment fallback. After the LOCAL/STATIC checks miss
    in emitAssign, the bottom path used to be a one-line WARN that
    emitted RHS + `Pop()` — silently discarding the value. PRIVATE
    pSum stayed at its initial value forever. Now: ASSIGN goes
    through PopMemvar; compound forms read via PushMemvar, fold,
    write back via PopMemvar.

Test fixture (tests/std_ch/test_compound_lhs.prg) covers all four
shapes. The std.ch runner picks it up so the regression suite now
stands at 15/15.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 15/15
  FRB suite          : 5/5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:14:28 +09:00
efb615bed9 fix(frb,genpc): in-process compile + 4 pcode bugs
Compiling _FiveSql2/test/test_sql_extreme.prg + a sweep of the FRB
demos surfaced four real bugs in the dynamic-compilation pipeline.
All fixes shipped together because they were on the same critical
path; each is independently revertible.

  * **pcode FOR loop ignored STEP and direction.** emitFor in
    compiler/genpc emitted a fixed `<= to` comparison and a hardcoded
    `+1` increment, then deleted the actual step expression with
    slice arithmetic on the byte buffer. Result: `FOR 5 TO 1 STEP
    -1` exited on the first iteration; `FOR 1 TO 10 STEP 2` summed
    1..10 (55) instead of 1+3+5+7+9 (25). Rewritten to mirror
    gengo's emitFor: detect negative step from a literal `-N` or
    unary MINUS, pick `<=` vs `>=` accordingly, and emit a clean
    `var := var + step` increment per iteration.

  * **pcode compound `+=` operator stored only the RHS.** emitAssign
    looked at AssignExpr.Op only for the := case; +=/-=/etc.
    silently took the same path, so `n += i` compiled as `n := i`,
    discarding the accumulator. Loop reduces were wrong: `Reverse`
    returned "" and `n := 0; FOR i ... n += i; NEXT` returned only
    the last increment. New compoundBinOp helper maps PLUSEQ /
    MINUSEQ / STAREQ / SLASHEQ / PERCENTEQ / POWEREQ to their
    matching binary opcode; emitAssign emits `local + rhs ; pop
    local` for compound forms.

  * **Pcode body stack leaks polluted the caller's frame.** A pcode
    function whose body left intermediate values on the data stack
    (FOR control values, etc.) returned with extra entries past
    its declared retVal. FrbDoFunc / FrbExecFunc / FrbRunFunc then
    pushed retVal on top of those leaks, so the caller saw the
    leaked values where its own preceding arguments should have
    been: `? "Fibonacci(10) =", FrbDo(...), "(expect 55)"` printed
    `1 55 (expect 55)` because the FOR loop's `1` lived in arg-1's
    slot. Two new Thread methods (`SP()` / `SetSP(int)`) let the
    three FRB dispatchers snapshot stack depth before the inner
    call and clamp it back afterward, so the leaks evaporate before
    they reach the caller's frame.

  * **FrbExec / FrbRun recursed into the host's Main forever.** Both
    looked up "MAIN" via t.VM().FindSymbol, which always resolved
    to the OUTER program's Main since FRB modules deliberately keep
    Main local. Compile + run + unload became compile + recurse +
    OOM. Both now look up Main via mod.FindFunc("MAIN") (module
    scope) — Frbload's policy of leaving Main module-local now
    actually has the intended effect.

Plus an architectural improvement: in-memory compilation no longer
depends on shelling out to an external `five` binary. New
hbrtl.frbCompileInProc parses + preprocesses + generates pcode in
process, building a FrbModule directly. FrbCompile and FrbExec use
this exclusively, which means dynamic compilation works from any
directory regardless of PATH and without a second process. The
plugin-mode path (with its runtime-version-mismatch fragility) is
left available via hbrt.FrbCompileSource for callers that want it,
but FrbCompile no longer reaches for it by default.

Test suite: tests/frb/ holds five fixtures + a runner. 5/5 pass:
test_frb_simple / test_frb_pcode_load / test_frb_compile /
test_frb_loop / test_frb_step.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 14/14
  FRB suite          : 5/5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:25:35 +09:00
412351b67d feat(rtl): LIST/DISPLAY TO FILE — text output redirection
Wire up TO FILE for both LIST and DISPLAY: __dbList grows a 9th
parameter cFile, opens it (truncating any prior content) when non-
empty, and writes the formatted rows there via fmt.Fprintln. Default
behavior (no TO FILE) still goes to stdout.

std.ch gets two new rules placed *before* the regular LIST/DISPLAY
patterns so they win when TO FILE is present:

  LIST    [<v,...>] TO FILE <(f)> [OFF] [FOR] [WHILE] [NEXT] ...
  DISPLAY [<v,...>] TO FILE <(f)> [OFF] [FOR] [WHILE] [NEXT] ...

Open failure raises a clear *HbError ("LIST/DISPLAY TO FILE: cannot
create <path> — <syscall reason>") so callers know exactly what went
wrong instead of getting partial-or-empty output.

TO PRINTER stays rejected via __dbNotImpl — Five doesn't drive a
printer port. Test coverage: tests/std_ch/test_list_to_file.prg
exercises four shapes (full LIST, single-row DISPLAY, OFF + FOR with
explicit fields, and confirms TO PRINTER still raises). Wired into
the std.ch runner so the regression suite now stands at 14/14.

Gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 14/14

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:15:32 +09:00
3a7f1dea72 feat(rtl,tests): pre-release UX round (Wave 5)
Three audit findings around polish + a release-readiness commit:

  * #UX1 LIST/DISPLAY output: dropped \r\n (unix terminals showed a
    stray ^M), moved the newline to AFTER each row (no more leading
    blank line), and added the `*` deleted-record marker after the
    record number — matches xBase LIST/DISPLAY convention. With
    SET DELETED ON the marker is unreachable since the row would
    have been skipped at Area.Skip level; with SET DELETED OFF the
    user now sees which rows are tombstoned.

  * #26 temp aliases: `__copytmp` / `__sorttmp` / `__totaltmp` /
    `__jointmp` were process-global string constants. A nested
    invocation (e.g., COPY inside a FOR clause whose expression
    runs another COPY) collided on the alias and the inner Open
    failed with "alias already in use" — surfacing as `.F.` with
    no clear cause. Each Open now goes through a new helper
    `nextTmpAlias(prefix)` backed by an atomic counter, so every
    call gets `__copytmp_1`, `__copytmp_2`, etc. — no collisions.

  * #J test coverage gap: the 13 std.ch regression tests were all
    sitting in `/tmp` — lost on tmpfs reboot, never in git, never
    in CI. Move them into `tests/std_ch/` and add a simple
    `run.sh` runner that builds + executes each one in a temp
    scratch directory and grep-asserts on FAIL / NOT REJECTED /
    expectation-mismatch markers. 13/13 pass against the current
    head:

       PASS  test_pp_stdch       PASS  test_count
       PASS  test_sum_avg        PASS  test_sum_multi
       PASS  test_copy           PASS  test_sort
       PASS  test_list           PASS  test_total
       PASS  test_join           PASS  test_update
       PASS  test_set_deleted    PASS  test_unsupported
       PASS  test_block_comma

    test_block_comma in particular guards the gengo SeqExpr fix
    from Wave 1 — without it the comma-in-block miscompile would
    silently come back.

Gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 13/13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:07:50 +09:00
dd270d5d9d perf: RTL Go-native migration — 27 optimizations, DML up to 70-90x
Systematic pass through PRG hot paths, promoting them to Go RTL while
preserving Harbour/FiveSql2 semantics. Full log in
docs/RTL-Go-Native-Migration.md.

Bench (bench_sql) vs 2026-04-08 baseline
 - B1  SELECT *             2,192 → 114   µs   (19x)
 - B6  INNER JOIN           9,291 → 233   µs   (40x)
 - B7  CTE simple           8,037 → 129   µs   (62x)
 - B9  ROW_NUMBER           3,705 → 265   µs   (14x)
 - B10 RANK PARTITION       4,748 → 309   µs   (15x)
 - B12 INSERT (WA cache)    4,319 →  63   µs   (69x)
 - B13 UPDATE (WA cache)    6,144 →  68   µs   (90x)
 - B15 CTE+WIN+JOIN        18,395 → 1,873 µs   (10x)

Infrastructure
 - HbHash O(1) Index preserving insertion order (Harbour KEEPORDER)
 - HbDeepClone Go RTL (scalar-sharing, immutable hash keys)
 - MEMRDD auto-imported via gengo; all Five programs get mem:name driver
 - SQL plan + pcode caches (s_hPlanCache, s_hDmlPcodeCache)
 - Opt-in SqlWACacheEnable — dbUseArea/Close/Commit batched for DML

SQL engine
 - FiveSql2 lexer ported to Go (byte FSM) with combined automatic
   template parameterization (literals → ?, concat queries share plan)
 - Go RTL: SqlDistinct, SqlGroupRows, SqlWindowPartitions,
   SqlWindowSortPartition, SqlWindowAssignRank, SqlComputeAggSimple,
   SqlBulkInsert, SqlBulkUpdate, SqlExprHasAgg, SqlEvalHaving
 - CTE / subquery / driving-table materialize paths use MEMRDD
 - SqlCoerce/SqlCmp/SqlIsTrue helpers moved from PRG to Go
 - SqlBulkUpdate defers Flush when WA cache active (APFS fsync was
   dominant B13 cost — 1.6ms/call → gone)

Correctness fixes uncovered during migration
 - ASort default path now sorts dates/logicals/timestamps (was no-op)
 - ORDER BY default NULL placement matches PRG SqlRowCompare across
   Go fast path; explicit NULLS FIRST/LAST honored by both paths
 - SqlBulkUpdate respects EXCLUSIVE vs SHARED mode record locks
 - SqlCmp/SqlCmpEq normalize NumInt vs Double (caught by test 6b)

Verification
 - go test ./...              ALL PASS
 - FiveSql2 test_sql1999      43/43
 - tests/compat_harbour       56/56 (+5 new: ASort dates/logicals,
                              AScan int cross-type)
 - Regression test test_null_order.prg for ORDER BY NULL ordering

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:20:14 +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
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