Files
five/hbrt/hbfunc.go
CharlesKWON 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

663 lines
18 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// hbfunc.go — Harbour HB_FUNC compatible Go API for #pragma BEGINDUMP.
//
// This provides the complete Harbour C extension API in Go,
// allowing PRG code to call inline Go functions seamlessly.
//
// Usage in PRG:
//
// #pragma BEGINDUMP
// func init() {
// hbrt.HB_FUNC("MYFUNC", MyFunc)
// }
// func MyFunc(ctx *hbrt.HBContext) {
// name := ctx.ParC(1)
// age := ctx.ParNI(2)
// ctx.RetC("Hello " + name)
// }
// #pragma ENDDUMP
//
// Then in PRG:
//
// ? MyFunc("Charles", 30) // → "Hello Charles"
package hbrt
import (
"fmt"
"strings"
"time"
)
// HBContext wraps Thread for Harbour-compatible C API access.
// Maps 1:1 to Harbour's hb_par*/hb_ret*/hb_stor* functions.
type HBContext struct {
T *Thread
}
// ---------------------------------------------------------------------------
// HB_FUNC registration — called from init() in #pragma BEGINDUMP
// ---------------------------------------------------------------------------
// HB_FUNC registers a Go function as a Harbour-callable function.
// Equivalent to Harbour's HB_FUNC(name) macro.
func HB_FUNC(name string, fn func(ctx *HBContext)) {
RegisterDynamicFunc(strings.ToUpper(name), func(t *Thread) {
ctx := &HBContext{T: t}
fn(ctx)
})
}
// HB_FUNC_STATIC is same as HB_FUNC but marks as static scope.
func HB_FUNC_STATIC(name string, fn func(ctx *HBContext)) {
HB_FUNC(name, fn)
}
// ---------------------------------------------------------------------------
// Parameter count — Harbour: hb_pcount()
// ---------------------------------------------------------------------------
func (c *HBContext) PCount() int {
return c.T.ParamCount()
}
// ---------------------------------------------------------------------------
// Parameter access (1-based index)
// ---------------------------------------------------------------------------
func (c *HBContext) param(n int) Value {
if n < 1 || n > c.PCount() {
return MakeNil()
}
return c.T.Local(n)
}
// Param returns raw Value of parameter n.
func (c *HBContext) Param(n int) Value { return c.param(n) }
// ---------------------------------------------------------------------------
// Type checking — Harbour: HB_IS*(n) macros
// ---------------------------------------------------------------------------
func (c *HBContext) IsNil(n int) bool { return c.param(n).IsNil() }
func (c *HBContext) IsChar(n int) bool { return c.param(n).IsString() }
func (c *HBContext) IsString(n int) bool { return c.param(n).IsString() }
func (c *HBContext) IsNum(n int) bool { return c.param(n).IsNumeric() }
func (c *HBContext) IsNumeric(n int) bool { return c.param(n).IsNumeric() }
func (c *HBContext) IsLog(n int) bool { return c.param(n).IsLogical() }
func (c *HBContext) IsLogical(n int) bool { return c.param(n).IsLogical() }
func (c *HBContext) IsDate(n int) bool { return c.param(n).IsDate() }
func (c *HBContext) IsDateTime(n int) bool { return c.param(n).IsDateTime() }
func (c *HBContext) IsArray(n int) bool { return c.param(n).IsArray() }
func (c *HBContext) IsHash(n int) bool { return c.param(n).IsHash() }
func (c *HBContext) IsBlock(n int) bool { return c.param(n).IsBlock() }
func (c *HBContext) IsObject(n int) bool { return c.param(n).IsObject() }
func (c *HBContext) IsPointer(n int) bool { return c.param(n).IsPointer() }
// ---------------------------------------------------------------------------
// String parameters — Harbour: hb_parc, hb_parclen
// ---------------------------------------------------------------------------
// ParC returns string parameter n. Harbour: hb_parc(n)
func (c *HBContext) ParC(n int) string {
v := c.param(n)
if v.IsString() {
return v.AsString()
}
return ""
}
// ParCLen returns length of string parameter n. Harbour: hb_parclen(n)
func (c *HBContext) ParCLen(n int) int {
v := c.param(n)
if v.IsString() {
return len(v.AsString())
}
return 0
}
// ---------------------------------------------------------------------------
// Numeric parameters — Harbour: hb_parni, hb_parnl, hb_parnd
// ---------------------------------------------------------------------------
// ParNI returns int parameter. Harbour: hb_parni(n)
func (c *HBContext) ParNI(n int) int {
v := c.param(n)
if v.IsNumeric() {
return v.AsInt()
}
return 0
}
// ParNIDef returns int parameter with default. Harbour: hb_parnidef(n, def)
func (c *HBContext) ParNIDef(n int, def int) int {
v := c.param(n)
if v.IsNumeric() {
return v.AsInt()
}
return def
}
// ParNL returns int64 parameter. Harbour: hb_parnl(n)
func (c *HBContext) ParNL(n int) int64 {
v := c.param(n)
if v.IsNumeric() {
return v.AsLong()
}
return 0
}
// ParNLDef returns int64 parameter with default. Harbour: hb_parnldef(n, def)
func (c *HBContext) ParNLDef(n int, def int64) int64 {
v := c.param(n)
if v.IsNumeric() {
return v.AsLong()
}
return def
}
// ParND returns float64 parameter. Harbour: hb_parnd(n)
func (c *HBContext) ParND(n int) float64 {
v := c.param(n)
if v.IsNumeric() {
return v.AsNumDouble()
}
return 0
}
// ParNDDef returns float64 parameter with default.
func (c *HBContext) ParNDDef(n int, def float64) float64 {
v := c.param(n)
if v.IsNumeric() {
return v.AsNumDouble()
}
return def
}
// ParNInt returns HB_MAXINT parameter. Harbour: hb_parnint(n)
func (c *HBContext) ParNInt(n int) int64 { return c.ParNL(n) }
// ---------------------------------------------------------------------------
// Logical parameters — Harbour: hb_parl
// ---------------------------------------------------------------------------
// ParL returns bool parameter. Harbour: hb_parl(n)
func (c *HBContext) ParL(n int) bool {
v := c.param(n)
if v.IsLogical() {
return v.AsBool()
}
return false
}
// ParLDef returns bool parameter with default. Harbour: hb_parldef(n, def)
func (c *HBContext) ParLDef(n int, def bool) bool {
v := c.param(n)
if v.IsLogical() {
return v.AsBool()
}
return def
}
// ---------------------------------------------------------------------------
// Date parameters — Harbour: hb_pards, hb_pardl
// ---------------------------------------------------------------------------
// julianToYMD converts Julian day to year, month, day.
func julianToYMD(julian int64) (int, int, int) {
if julian <= 0 {
return 0, 0, 0
}
l := julian + 68569
n := 4 * l / 146097
l = l - (146097*n+3)/4
i := 4000 * (l + 1) / 1461001
l = l - 1461*i/4 + 31
j := 80 * l / 2447
d := l - 2447*j/80
l = j / 11
m := j + 2 - 12*l
y := 100*(n-49) + i + l
return int(y), int(m), int(d)
}
// ymdToJulian converts year, month, day to Julian day number.
func ymdToJulian(y, m, d int) int64 {
if y == 0 && m == 0 && d == 0 {
return 0
}
mm := int64(m)
yy := int64(y)
dd := int64(d)
return dd - 32075 +
1461*(yy+4800+(mm-14)/12)/4 +
367*(mm-2-(mm-14)/12*12)/12 -
3*((yy+4900+(mm-14)/12)/100)/4
}
// ParDS returns date as "YYYYMMDD" string. Harbour: hb_pards(n)
func (c *HBContext) ParDS(n int) string {
v := c.param(n)
if v.IsDate() {
y, m, d := julianToYMD(v.AsJulian())
return fmt.Sprintf("%04d%02d%02d", y, m, d)
}
return " "
}
// ParDL returns date as Julian day number. Harbour: hb_pardl(n)
func (c *HBContext) ParDL(n int) int64 {
v := c.param(n)
if v.IsDate() {
return v.AsJulian()
}
return 0
}
// ParDate returns date as Go time.Time (Five extension).
func (c *HBContext) ParDate(n int) time.Time {
v := c.param(n)
if v.IsDate() {
y, m, d := julianToYMD(v.AsJulian())
return time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Local)
}
return time.Time{}
}
// ---------------------------------------------------------------------------
// Array parameters
// ---------------------------------------------------------------------------
// ParArray returns array items. Five extension.
func (c *HBContext) ParArray(n int) []Value {
v := c.param(n)
if v.IsArray() {
return v.AsArray().Items
}
return nil
}
// ParArrayLen returns array length. Harbour: hb_parinfa(n, 0)
func (c *HBContext) ParArrayLen(n int) int {
v := c.param(n)
if v.IsArray() {
return len(v.AsArray().Items)
}
return 0
}
// ParHash returns hash. Five extension.
func (c *HBContext) ParHash(n int) *HbHash {
v := c.param(n)
if v.IsHash() {
return v.AsHash()
}
return nil
}
// ---------------------------------------------------------------------------
// Return values — Harbour: hb_ret*
// ---------------------------------------------------------------------------
// Ret returns NIL. Harbour: hb_ret()
func (c *HBContext) Ret() {
c.T.PushNil()
c.T.RetValue()
}
// RetNil returns NIL explicitly.
func (c *HBContext) RetNil() {
c.T.PushNil()
c.T.RetValue()
}
// RetC returns string. Harbour: hb_retc(s)
func (c *HBContext) RetC(s string) {
c.T.PushString(s)
c.T.RetValue()
}
// RetCLen returns string of specific length. Harbour: hb_retclen(s, n)
func (c *HBContext) RetCLen(s string, n int) {
if n < len(s) {
s = s[:n]
}
c.T.PushString(s)
c.T.RetValue()
}
// RetNI returns integer. Harbour: hb_retni(n)
func (c *HBContext) RetNI(n int) {
c.T.PushInt(n)
c.T.RetValue()
}
// RetNL returns long. Harbour: hb_retnl(n)
func (c *HBContext) RetNL(n int64) {
c.T.PushLong(n)
c.T.RetValue()
}
// RetND returns double. Harbour: hb_retnd(d)
func (c *HBContext) RetND(d float64) {
c.T.PushDouble(d, 0, 0)
c.T.RetValue()
}
// RetNDLen returns double with width/decimals. Harbour: hb_retndlen(d, w, dec)
func (c *HBContext) RetNDLen(d float64, width, dec int) {
c.T.PushDouble(d, uint16(width), uint16(dec))
c.T.RetValue()
}
// RetL returns logical. Harbour: hb_retl(b)
func (c *HBContext) RetL(b bool) {
c.T.PushBool(b)
c.T.RetValue()
}
// RetDS returns date from "YYYYMMDD". Harbour: hb_retds(s)
func (c *HBContext) RetDS(s string) {
if len(s) >= 8 {
y, m, d := 0, 0, 0
fmt.Sscanf(s, "%04d%02d%02d", &y, &m, &d)
c.T.PushValue(MakeDate(ymdToJulian(y, m, d)))
} else {
c.T.PushValue(MakeDate(0))
}
c.T.RetValue()
}
// RetDL returns date from Julian. Harbour: hb_retdl(n)
func (c *HBContext) RetDL(julian int64) {
c.T.PushValue(MakeDate(julian))
c.T.RetValue()
}
// RetD returns date from y/m/d. Harbour: hb_retd(y, m, d)
func (c *HBContext) RetD(y, m, d int) {
c.T.PushValue(MakeDate(ymdToJulian(y, m, d)))
c.T.RetValue()
}
// RetValue returns raw Value. Five extension.
func (c *HBContext) RetVal(v Value) {
c.T.PushValue(v)
c.T.RetValue()
}
// RetA returns empty array of size n. Harbour: hb_reta(n)
func (c *HBContext) RetA(size int) {
c.T.PushValue(MakeArray(size))
c.T.RetValue()
}
// RetArray returns populated array. Five extension.
func (c *HBContext) RetArray(items []Value) {
c.T.PushValue(MakeArrayFrom(items))
c.T.RetValue()
}
// RetHash returns hash. Five extension.
func (c *HBContext) RetHash(h *HbHash) {
c.T.PushValue(MakeHashFrom(h))
c.T.RetValue()
}
// ---------------------------------------------------------------------------
// By-reference storage — Harbour: hb_stor*
// ---------------------------------------------------------------------------
// StorNil stores NIL into by-ref param. Harbour: hb_stor(n)
func (c *HBContext) StorNil(n int) {
if n >= 1 && n <= c.PCount() {
c.T.SetLocal(n, MakeNil())
}
}
// StorC stores string into by-ref param. Harbour: hb_storc(s, n)
func (c *HBContext) StorC(s string, n int) {
if n >= 1 && n <= c.PCount() {
c.T.SetLocal(n, MakeString(s))
}
}
// StorNI stores int into by-ref param. Harbour: hb_storni(v, n)
func (c *HBContext) StorNI(v int, n int) {
if n >= 1 && n <= c.PCount() {
c.T.SetLocal(n, MakeInt(v))
}
}
// StorNL stores int64 into by-ref param. Harbour: hb_stornl(v, n)
func (c *HBContext) StorNL(v int64, n int) {
if n >= 1 && n <= c.PCount() {
c.T.SetLocal(n, MakeLong(v))
}
}
// StorND stores float64 into by-ref param. Harbour: hb_stornd(v, n)
func (c *HBContext) StorND(v float64, n int) {
if n >= 1 && n <= c.PCount() {
c.T.SetLocal(n, MakeDouble(v, 0, 0))
}
}
// StorL stores bool into by-ref param. Harbour: hb_storl(v, n)
func (c *HBContext) StorL(v bool, n int) {
if n >= 1 && n <= c.PCount() {
c.T.SetLocal(n, MakeBool(v))
}
}
// StorDS stores date string into by-ref param. Harbour: hb_stords(s, n)
func (c *HBContext) StorDS(s string, n int) {
if n >= 1 && n <= c.PCount() && len(s) >= 8 {
y, m, d := 0, 0, 0
fmt.Sscanf(s, "%04d%02d%02d", &y, &m, &d)
c.T.SetLocal(n, MakeDate(ymdToJulian(y, m, d)))
}
}
// StorDL stores Julian date into by-ref param. Harbour: hb_stordl(v, n)
func (c *HBContext) StorDL(v int64, n int) {
if n >= 1 && n <= c.PCount() {
c.T.SetLocal(n, MakeDate(v))
}
}
// ---------------------------------------------------------------------------
// Array manipulation — Harbour: hb_array*
// ---------------------------------------------------------------------------
// ArrayNew creates empty array. Harbour: hb_arrayNew()
func (c *HBContext) ArrayNew(size int) Value {
return MakeArray(size)
}
// ArrayLen returns array length. Harbour: hb_arrayLen()
func (c *HBContext) ArrayLen(v Value) int {
if v.IsArray() {
return len(v.AsArray().Items)
}
return 0
}
// ArrayGet gets element at 1-based index. Harbour: hb_arrayGet()
func (c *HBContext) ArrayGet(v Value, index int) Value {
if v.IsArray() {
items := v.AsArray().Items
if index >= 1 && index <= len(items) {
return items[index-1]
}
}
return MakeNil()
}
// ArrayGetC gets string at index. Harbour: hb_arrayGetC()
func (c *HBContext) ArrayGetC(v Value, index int) string {
return c.ArrayGet(v, index).AsString()
}
// ArrayGetNI gets int at index. Harbour: hb_arrayGetNI()
func (c *HBContext) ArrayGetNI(v Value, index int) int {
return c.ArrayGet(v, index).AsInt()
}
// ArrayGetND gets double at index. Harbour: hb_arrayGetND()
func (c *HBContext) ArrayGetND(v Value, index int) float64 {
return c.ArrayGet(v, index).AsNumDouble()
}
// ArrayGetL gets bool at index. Harbour: hb_arrayGetL()
func (c *HBContext) ArrayGetL(v Value, index int) bool {
return c.ArrayGet(v, index).AsBool()
}
// ArraySet sets element at 1-based index. Harbour: hb_arraySet()
func (c *HBContext) ArraySet(v Value, index int, item Value) {
if v.IsArray() {
items := v.AsArray().Items
if index >= 1 && index <= len(items) {
items[index-1] = item
}
}
}
// ArraySetC sets string at index. Harbour: hb_arraySetC()
func (c *HBContext) ArraySetC(v Value, index int, s string) {
c.ArraySet(v, index, MakeString(s))
}
// ArraySetNI sets int at index. Harbour: hb_arraySetNI()
func (c *HBContext) ArraySetNI(v Value, index int, n int) {
c.ArraySet(v, index, MakeInt(n))
}
// ArraySetND sets double at index. Harbour: hb_arraySetND()
func (c *HBContext) ArraySetND(v Value, index int, d float64) {
c.ArraySet(v, index, MakeDouble(d, 0, 0))
}
// ArraySetL sets bool at index. Harbour: hb_arraySetL()
func (c *HBContext) ArraySetL(v Value, index int, b bool) {
c.ArraySet(v, index, MakeBool(b))
}
// ArrayAdd appends to array. Harbour: hb_arrayAdd()
func (c *HBContext) ArrayAdd(v Value, item Value) {
if v.IsArray() {
arr := v.AsArray()
arr.Items = append(arr.Items, item)
}
}
// ---------------------------------------------------------------------------
// Hash manipulation — Harbour: hb_hash*
// ---------------------------------------------------------------------------
// HashNew creates empty hash.
func (c *HBContext) HashNew() Value {
return MakeHash()
}
// HashLen returns hash size.
func (c *HBContext) HashLen(v Value) int {
if v.IsHash() {
return len(v.AsHash().Keys)
}
return 0
}
// HashAdd adds key-value pair. Harbour: hb_hashAdd()
func (c *HBContext) HashAdd(v Value, key, val Value) {
if v.IsHash() {
v.AsHash().Set(key, val)
}
}
// HashGetC gets value by string key. Five extension.
// Hits the Index directly with the "S"+key serialization so we skip
// allocating a Value wrapper for the lookup.
func (c *HBContext) HashGetC(v Value, key string) Value {
if v.IsHash() {
h := v.AsHash()
h.ensureIndex()
if i, ok := h.Index["S"+key]; ok {
return h.Values[i]
}
}
return MakeNil()
}
// ---------------------------------------------------------------------------
// Error handling — Harbour: hb_errRT_BASE
// ---------------------------------------------------------------------------
// ErrRT_BASE raises a BASE runtime error.
func (c *HBContext) ErrRT_BASE(subCode int, description, operation string) {
panic(fmt.Sprintf("BASE/%04d: %s: %s", subCode, description, operation))
}
// ErrRT_BASE_SubstR raises a substitution error.
func (c *HBContext) ErrRT_BASE_SubstR(subCode int, description, operation string) {
c.ErrRT_BASE(subCode, description, operation)
}
// ---------------------------------------------------------------------------
// ParInfo — Harbour: hb_parinfo(n)
// ---------------------------------------------------------------------------
const (
HB_IT_NIL = 0x00001
HB_IT_INTEGER = 0x00002
HB_IT_LONG = 0x00008
HB_IT_DOUBLE = 0x00010
HB_IT_DATE = 0x00020
HB_IT_TIMESTAMP = 0x00040
HB_IT_LOGICAL = 0x00080
HB_IT_SYMBOL = 0x00100
HB_IT_POINTER = 0x00200
HB_IT_STRING = 0x00400
HB_IT_MEMO = 0x00800
HB_IT_BLOCK = 0x01000
HB_IT_BYREF = 0x02000
HB_IT_ARRAY = 0x04000
HB_IT_HASH = 0x08000
HB_IT_OBJECT = 0x10000
HB_IT_NUMERIC = HB_IT_INTEGER | HB_IT_LONG | HB_IT_DOUBLE
)
// ParInfo returns type flags for parameter n. Harbour: hb_parinfo(n)
func (c *HBContext) ParInfo(n int) int {
v := c.param(n)
switch {
case v.IsNil():
return HB_IT_NIL
case v.IsString():
return HB_IT_STRING
case v.IsLogical():
return HB_IT_LOGICAL
case v.IsDate():
return HB_IT_DATE
case v.IsTimestamp():
return HB_IT_TIMESTAMP
case v.IsArray():
if v.IsObject() {
return HB_IT_OBJECT
}
return HB_IT_ARRAY
case v.IsHash():
return HB_IT_HASH
case v.IsBlock():
return HB_IT_BLOCK
case v.IsPointer():
return HB_IT_POINTER
case v.IsNumeric():
return HB_IT_NUMERIC
default:
return HB_IT_NIL
}
}