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>
399 lines
10 KiB
Plaintext
399 lines
10 KiB
Plaintext
/*
|
|
* compat_harbour.prg — Harbour Compatibility Test Suite for Five
|
|
*
|
|
* Tests language features that differ between Harbour and Go semantics.
|
|
* Run: five build tests/compat_harbour.prg -o test_compat && ./test_compat
|
|
*
|
|
* Copyright (c) 2026 Charles KWON OhJun
|
|
*/
|
|
|
|
STATIC s_nPass := 0
|
|
STATIC s_nFail := 0
|
|
|
|
PROCEDURE Main()
|
|
|
|
? "================================================================"
|
|
? " Five — Harbour Compatibility Test Suite"
|
|
? "================================================================"
|
|
?
|
|
|
|
TestByref()
|
|
TestShortCircuit()
|
|
TestForLoop()
|
|
TestSequence()
|
|
TestClosure()
|
|
TestTypes()
|
|
TestStatic()
|
|
TestLocalScope()
|
|
TestArrayHash()
|
|
|
|
?
|
|
? "================================================================"
|
|
? " Results:", hb_ntos(s_nPass), "/", hb_ntos(s_nPass + s_nFail), "passed"
|
|
? "================================================================"
|
|
|
|
IF s_nFail > 0
|
|
ERRORLEVEL(1)
|
|
ENDIF
|
|
|
|
RETURN
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 1. @byref pass-by-reference */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestByref()
|
|
|
|
LOCAL n := 10, cStr := "hello"
|
|
|
|
? "--- 1. @byref ---"
|
|
|
|
// Basic
|
|
ByrefModify(@n)
|
|
Assert("1a @byref basic: n changed to 42", n == 42)
|
|
|
|
// Chained
|
|
n := 100
|
|
ByrefMiddle(@n)
|
|
Assert("1b @byref chained: n changed to 999", n == 999)
|
|
|
|
// Loop accumulation
|
|
LOCAL nSum := 0, i
|
|
FOR i := 1 TO 5
|
|
ByrefAdd(@nSum, i)
|
|
NEXT
|
|
Assert("1c @byref loop: sum 1..5 = 15", nSum == 15)
|
|
|
|
// String
|
|
ByrefAppend(@cStr, " world")
|
|
Assert("1d @byref string: 'hello world'", cStr == "hello world")
|
|
|
|
RETURN
|
|
|
|
STATIC FUNCTION ByrefModify(x)
|
|
x := 42
|
|
RETURN NIL
|
|
|
|
STATIC FUNCTION ByrefMiddle(x)
|
|
ByrefInner(@x)
|
|
RETURN NIL
|
|
|
|
STATIC FUNCTION ByrefInner(y)
|
|
y := 999
|
|
RETURN NIL
|
|
|
|
STATIC FUNCTION ByrefAdd(nAcc, nVal)
|
|
nAcc := nAcc + nVal
|
|
RETURN NIL
|
|
|
|
STATIC FUNCTION ByrefAppend(cP, cS)
|
|
cP := cP + cS
|
|
RETURN NIL
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 2. Short-circuit AND/OR */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestShortCircuit()
|
|
|
|
LOCAL lCalled
|
|
|
|
? "--- 2. Short-circuit AND/OR ---"
|
|
|
|
// .AND. short-circuits on false left
|
|
lCalled := .F.
|
|
IF .F. .AND. SideEffect(@lCalled)
|
|
ENDIF
|
|
Assert("2a AND short-circuit: right not called", ! lCalled)
|
|
|
|
// .OR. short-circuits on true left
|
|
lCalled := .F.
|
|
IF .T. .OR. SideEffect(@lCalled)
|
|
ENDIF
|
|
Assert("2b OR short-circuit: right not called", ! lCalled)
|
|
|
|
// .AND. evaluates right when left is true
|
|
lCalled := .F.
|
|
IF .T. .AND. SideEffect(@lCalled)
|
|
ENDIF
|
|
Assert("2c AND evaluates right when left=.T.", lCalled)
|
|
|
|
// NIL in condition → .F.
|
|
Assert("2d NIL .AND. .T. = .F.", ! (NIL .AND. .T.))
|
|
|
|
RETURN
|
|
|
|
STATIC FUNCTION SideEffect(lFlag)
|
|
lFlag := .T.
|
|
RETURN .T.
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 3. FOR..NEXT LOOP/EXIT */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestForLoop()
|
|
|
|
LOCAL i, n, nLoopCount
|
|
|
|
? "--- 3. FOR..NEXT LOOP ---"
|
|
|
|
// Basic FOR
|
|
n := 0
|
|
FOR i := 1 TO 5
|
|
n += i
|
|
NEXT
|
|
Assert("3a FOR sum 1..5 = 15", n == 15)
|
|
|
|
// FOR with EXIT
|
|
n := 0
|
|
FOR i := 1 TO 100
|
|
IF i > 5
|
|
EXIT
|
|
ENDIF
|
|
n += i
|
|
NEXT
|
|
Assert("3b FOR EXIT: sum 1..5 = 15", n == 15)
|
|
|
|
// FOR with LOOP (LOOP goes to NEXT, increments counter)
|
|
nLoopCount := 0
|
|
FOR i := 1 TO 5
|
|
nLoopCount++
|
|
IF i == 3
|
|
LOOP // should skip to NEXT (i becomes 4)
|
|
ENDIF
|
|
NEXT
|
|
Assert("3c FOR LOOP: counter = 5 (not infinite)", nLoopCount == 5)
|
|
|
|
// FOR STEP -1
|
|
n := 0
|
|
FOR i := 5 TO 1 STEP -1
|
|
n += i
|
|
NEXT
|
|
Assert("3d FOR STEP -1: sum 5..1 = 15", n == 15)
|
|
|
|
RETURN
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 4. BEGIN SEQUENCE / RECOVER */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestSequence()
|
|
|
|
LOCAL lRecovered, nResult
|
|
|
|
? "--- 4. BEGIN SEQUENCE ---"
|
|
|
|
// Basic recover
|
|
lRecovered := .F.
|
|
BEGIN SEQUENCE
|
|
nResult := 1 / 0 // division by zero
|
|
RECOVER
|
|
lRecovered := .T.
|
|
END SEQUENCE
|
|
Assert("4a RECOVER catches error", lRecovered)
|
|
|
|
// Normal flow (no error)
|
|
lRecovered := .F.
|
|
nResult := 0
|
|
BEGIN SEQUENCE
|
|
nResult := 42
|
|
RECOVER
|
|
lRecovered := .T.
|
|
END SEQUENCE
|
|
Assert("4b No error: result = 42", nResult == 42 .AND. ! lRecovered)
|
|
|
|
// Nested SEQUENCE
|
|
LOCAL lOuter := .F., lInner := .F.
|
|
BEGIN SEQUENCE
|
|
BEGIN SEQUENCE
|
|
nResult := 1 / 0
|
|
RECOVER
|
|
lInner := .T.
|
|
END SEQUENCE
|
|
RECOVER
|
|
lOuter := .T.
|
|
END SEQUENCE
|
|
Assert("4c Nested: inner caught, outer not", lInner .AND. ! lOuter)
|
|
|
|
RETURN
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 5. Code block closure capture */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestClosure()
|
|
|
|
LOCAL bBlock, nOuter := 10
|
|
|
|
? "--- 5. Closure ---"
|
|
|
|
// Basic capture
|
|
bBlock := {|| nOuter * 2}
|
|
Assert("5a Closure captures outer: 10*2=20", Eval(bBlock) == 20)
|
|
|
|
// Capture with parameter
|
|
bBlock := {|x| nOuter + x}
|
|
Assert("5b Closure with param: 10+5=15", Eval(bBlock, 5) == 15)
|
|
|
|
// Closure returning value
|
|
bBlock := {|a,b| a + b}
|
|
Assert("5c Closure 2 params: 3+7=10", Eval(bBlock, 3, 7) == 10)
|
|
|
|
RETURN
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 6. Type system */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestTypes()
|
|
|
|
? "--- 6. Types ---"
|
|
|
|
Assert("6a ValType(NIL) = 'U'", ValType(NIL) == "U")
|
|
Assert("6b ValType(1) = 'N'", ValType(1) == "N")
|
|
Assert("6c ValType('a') = 'C'", ValType("a") == "C")
|
|
Assert("6d ValType(.T.) = 'L'", ValType(.T.) == "L")
|
|
Assert("6e ValType({}) = 'A'", ValType({}) == "A")
|
|
Assert("6f ValType({=>}) = 'H'", ValType({=>}) == "H")
|
|
Assert("6g ValType({||}) = 'B'", ValType({|| NIL}) == "B")
|
|
Assert("6h NIL == NIL", NIL == NIL)
|
|
Assert("6i NIL != 0", !( NIL == 0 ))
|
|
Assert("6j Empty('')", Empty(""))
|
|
Assert("6k Empty(0)", Empty(0))
|
|
Assert("6l ! Empty(1)", ! Empty(1))
|
|
|
|
RETURN
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 7. STATIC variables */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestStatic()
|
|
|
|
? "--- 7. STATIC ---"
|
|
|
|
Assert("7a STATIC counter 1st call = 1", StaticCounter() == 1)
|
|
Assert("7b STATIC counter 2nd call = 2", StaticCounter() == 2)
|
|
Assert("7c STATIC counter 3rd call = 3", StaticCounter() == 3)
|
|
|
|
RETURN
|
|
|
|
STATIC s_nCounter := 0
|
|
|
|
STATIC FUNCTION StaticCounter()
|
|
s_nCounter++
|
|
RETURN s_nCounter
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 8. LOCAL scope */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestLocalScope()
|
|
|
|
LOCAL x := "outer"
|
|
|
|
? "--- 8. LOCAL scope ---"
|
|
|
|
Assert("8a LOCAL before IF: 'outer'", x == "outer")
|
|
Assert("8b LOCAL after assignment: unchanged", x == "outer")
|
|
|
|
// Multiple LOCALs in same function
|
|
LOCAL y := 99
|
|
Assert("8b Multiple LOCALs: y=99", y == 99)
|
|
Assert("8c x still 'outer'", x == "outer")
|
|
|
|
RETURN
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* 9. Array + Hash operations */
|
|
/* ====================================================================== */
|
|
STATIC PROCEDURE TestArrayHash()
|
|
|
|
LOCAL a, h, i, nSum
|
|
|
|
? "--- 9. Array + Hash ---"
|
|
|
|
// Array basics
|
|
a := {1, 2, 3}
|
|
Assert("9a Array literal: Len=3", Len(a) == 3)
|
|
AAdd(a, 4)
|
|
Assert("9b AAdd: Len=4", Len(a) == 4)
|
|
|
|
// ASort
|
|
a := {3, 1, 2}
|
|
ASort(a)
|
|
Assert("9c ASort: {1,2,3}", a[1] == 1 .AND. a[2] == 2 .AND. a[3] == 3)
|
|
|
|
// ASort with block
|
|
a := {3, 1, 2}
|
|
ASort(a,,, {|x,y| x > y})
|
|
Assert("9d ASort desc: {3,2,1}", a[1] == 3 .AND. a[2] == 2 .AND. a[3] == 1)
|
|
|
|
// ASort dates (default, no block — formerly no-op, now sorts julian)
|
|
a := { CToD("2026-03-15"), CToD("2024-01-10"), CToD("2025-07-01") }
|
|
ASort(a)
|
|
Assert("9c1 ASort dates ascending", ;
|
|
a[1] == CToD("2024-01-10") .AND. ;
|
|
a[2] == CToD("2025-07-01") .AND. ;
|
|
a[3] == CToD("2026-03-15"))
|
|
|
|
// ASort logicals (default — .F. < .T.)
|
|
a := { .T., .F., .T., .F. }
|
|
ASort(a)
|
|
Assert("9c2 ASort logicals: F,F,T,T", ;
|
|
!a[1] .AND. !a[2] .AND. a[3] .AND. a[4])
|
|
|
|
// AScan
|
|
a := {"alice", "bob", "charlie"}
|
|
Assert("9e AScan: found 'bob' at 2", AScan(a, "bob") == 2)
|
|
Assert("9f AScan: 'dave' not found", AScan(a, "dave") == 0)
|
|
|
|
// AScan numeric fast-path
|
|
a := { 10, 20, 30, 40 }
|
|
Assert("9e1 AScan int found", AScan(a, 30) == 3)
|
|
Assert("9e2 AScan int cross-type (double lookup)", AScan(a, 30.0) == 3)
|
|
Assert("9e3 AScan int not found", AScan(a, 99) == 0)
|
|
|
|
// AEval with mutable closure capture (Harbour: closures share outer locals)
|
|
nSum := 0
|
|
AEval({10, 20, 30}, {|x| nSum += x})
|
|
Assert("9g AEval closure sum: 60", nSum == 60)
|
|
|
|
// Hash basics
|
|
h := {=>}
|
|
h["name"] := "Alice"
|
|
h["age"] := 30
|
|
Assert("9h Hash set/get: 'Alice'", h["name"] == "Alice")
|
|
Assert("9i Hash Len: 2", Len(h) == 2)
|
|
Assert("9j hb_HHasKey: .T.", hb_HHasKey(h, "name"))
|
|
Assert("9k hb_HHasKey missing: .F.", ! hb_HHasKey(h, "xyz"))
|
|
|
|
// Hash iteration
|
|
LOCAL aKeys := hb_HKeys(h)
|
|
Assert("9l hb_HKeys Len: 2", Len(aKeys) == 2)
|
|
|
|
// hb_HClone
|
|
LOCAL h2 := hb_HClone(h)
|
|
h2["name"] := "Bob"
|
|
Assert("9m hb_HClone: orig unchanged", h["name"] == "Alice")
|
|
Assert("9n hb_HClone: clone changed", h2["name"] == "Bob")
|
|
|
|
RETURN
|
|
|
|
|
|
/* ====================================================================== */
|
|
/* Assert helper */
|
|
/* ====================================================================== */
|
|
STATIC FUNCTION Assert(cLabel, lOK)
|
|
|
|
IF lOK
|
|
s_nPass++
|
|
? " PASS:", cLabel
|
|
ELSE
|
|
s_nFail++
|
|
? " FAIL:", cLabel
|
|
ENDIF
|
|
|
|
RETURN NIL
|