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>
443 lines
12 KiB
Go
443 lines
12 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// gobridge.go — Bridge between Harbour Values and native Go objects.
|
|
//
|
|
// Allows PRG code to hold and manipulate Go objects (sql.DB, http.Client, etc.)
|
|
// using Harbour's object syntax: obj:Method(args)
|
|
//
|
|
// Architecture:
|
|
// PRG: db := sql.Open("sqlite", ":memory:") → Value wrapping *sql.DB
|
|
// PRG: db:Exec("CREATE TABLE ...") → reflect.Call on *sql.DB.Exec
|
|
// PRG: db:Close() → reflect.Call on *sql.DB.Close
|
|
//
|
|
// Type coercion: Value ↔ Go types automatic conversion
|
|
// string ↔ Value.AsString()
|
|
// int ↔ Value.AsInt()
|
|
// float64 ↔ Value.AsNumDouble()
|
|
// bool ↔ Value.AsBool()
|
|
// error ↔ Value (string of error message, or NIL)
|
|
// nil ↔ Value.IsNil()
|
|
|
|
package hbrt
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GoValue — wraps any Go object inside a Harbour Value
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// WrapGo wraps any Go value (pointer, interface, etc.) as a Harbour Value.
|
|
// The Go object is stored in Value.ptr as interface{}.
|
|
func WrapGo(obj interface{}) Value {
|
|
if obj == nil {
|
|
return MakeNil()
|
|
}
|
|
return MakePointer(obj)
|
|
}
|
|
|
|
// WrapGoError wraps a Go error as a Harbour Value.
|
|
// nil error → Harbour NIL, non-nil → Harbour string.
|
|
func WrapGoError(err error) Value {
|
|
if err == nil {
|
|
return MakeNil()
|
|
}
|
|
return MakeString(err.Error())
|
|
}
|
|
|
|
// UnwrapGo extracts the Go object from a Harbour Value.
|
|
func UnwrapGo(v Value) interface{} {
|
|
if v.IsPointer() {
|
|
return v.AsPointer()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GoCall — call a Go method on a wrapped object using reflection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GoCall calls method `name` on Go object wrapped in `receiver` with args.
|
|
// Returns array of Harbour Values (one per Go return value).
|
|
//
|
|
// PRG: result := obj:Method(arg1, arg2)
|
|
// Go: GoCall(objValue, "Method", arg1Value, arg2Value)
|
|
func GoCall(receiver Value, method string, args ...Value) []Value {
|
|
obj := UnwrapGo(receiver)
|
|
if obj == nil {
|
|
return []Value{MakeNil(), MakeString("nil receiver")}
|
|
}
|
|
|
|
rv := reflect.ValueOf(obj)
|
|
m := rv.MethodByName(method)
|
|
if !m.IsValid() {
|
|
// Try pointer receiver
|
|
if rv.Kind() != reflect.Ptr {
|
|
pv := reflect.New(rv.Type())
|
|
pv.Elem().Set(rv)
|
|
m = pv.MethodByName(method)
|
|
}
|
|
if !m.IsValid() {
|
|
return []Value{MakeNil(), MakeString("method not found: " + method)}
|
|
}
|
|
}
|
|
|
|
mt := m.Type()
|
|
|
|
// Convert Harbour args → Go args
|
|
goArgs := make([]reflect.Value, len(args))
|
|
for i, arg := range args {
|
|
if i < mt.NumIn() {
|
|
goArgs[i] = valueToReflect(arg, mt.In(i))
|
|
} else if mt.IsVariadic() && i >= mt.NumIn()-1 {
|
|
// Variadic: convert to slice element type
|
|
elemType := mt.In(mt.NumIn() - 1).Elem()
|
|
goArgs[i] = valueToReflect(arg, elemType)
|
|
} else {
|
|
goArgs[i] = reflect.ValueOf(valueToInterface(arg))
|
|
}
|
|
}
|
|
|
|
// Call — m.Call handles both variadic and non-variadic correctly
|
|
results := m.Call(goArgs)
|
|
|
|
// Convert Go results → Harbour Values
|
|
hbResults := make([]Value, len(results))
|
|
for i, r := range results {
|
|
hbResults[i] = reflectToValue(r)
|
|
}
|
|
|
|
return hbResults
|
|
}
|
|
|
|
// GoCallFunc calls a package-level Go function.
|
|
// fn must be a reflect.Value of the function.
|
|
func GoCallFunc(fn interface{}, args ...Value) []Value {
|
|
rv := reflect.ValueOf(fn)
|
|
if rv.Kind() != reflect.Func {
|
|
return []Value{MakeNil(), MakeString("not a function")}
|
|
}
|
|
|
|
ft := rv.Type()
|
|
goArgs := make([]reflect.Value, len(args))
|
|
isVariadic := ft.IsVariadic()
|
|
fixedCount := ft.NumIn()
|
|
if isVariadic {
|
|
fixedCount-- // last param is the variadic slice
|
|
}
|
|
for i, arg := range args {
|
|
if i < fixedCount {
|
|
goArgs[i] = valueToReflect(arg, ft.In(i))
|
|
} else if isVariadic {
|
|
// Variadic: convert to the slice's element type
|
|
elemType := ft.In(ft.NumIn() - 1).Elem()
|
|
goArgs[i] = valueToReflect(arg, elemType)
|
|
} else {
|
|
goArgs[i] = reflect.ValueOf(valueToInterface(arg))
|
|
}
|
|
}
|
|
|
|
results := rv.Call(goArgs)
|
|
hbResults := make([]Value, len(results))
|
|
for i, r := range results {
|
|
hbResults[i] = reflectToValue(r)
|
|
}
|
|
return hbResults
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GoGet / GoSet — field access on Go structs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GoGet gets a field value from a Go struct.
|
|
func GoGet(receiver Value, field string) Value {
|
|
obj := UnwrapGo(receiver)
|
|
if obj == nil {
|
|
return MakeNil()
|
|
}
|
|
rv := reflect.ValueOf(obj)
|
|
if rv.Kind() == reflect.Ptr {
|
|
rv = rv.Elem()
|
|
}
|
|
if rv.Kind() != reflect.Struct {
|
|
return MakeNil()
|
|
}
|
|
f := rv.FieldByName(field)
|
|
if !f.IsValid() {
|
|
return MakeNil()
|
|
}
|
|
return reflectToValue(f)
|
|
}
|
|
|
|
// GoSet sets a field value on a Go struct.
|
|
func GoSet(receiver Value, field string, val Value) {
|
|
obj := UnwrapGo(receiver)
|
|
if obj == nil {
|
|
return
|
|
}
|
|
rv := reflect.ValueOf(obj)
|
|
if rv.Kind() == reflect.Ptr {
|
|
rv = rv.Elem()
|
|
}
|
|
if rv.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
f := rv.FieldByName(field)
|
|
if !f.IsValid() || !f.CanSet() {
|
|
return
|
|
}
|
|
f.Set(valueToReflect(val, f.Type()))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Type coercion: Value → reflect.Value
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func valueToReflect(v Value, targetType reflect.Type) reflect.Value {
|
|
// Handle interface{} target
|
|
if targetType.Kind() == reflect.Interface {
|
|
return reflect.ValueOf(valueToInterface(v))
|
|
}
|
|
|
|
switch targetType.Kind() {
|
|
case reflect.String:
|
|
return reflect.ValueOf(v.AsString())
|
|
case reflect.Int:
|
|
return reflect.ValueOf(int(valToInt64(v)))
|
|
case reflect.Int8:
|
|
return reflect.ValueOf(int8(valToInt64(v)))
|
|
case reflect.Int16:
|
|
return reflect.ValueOf(int16(valToInt64(v)))
|
|
case reflect.Int32:
|
|
return reflect.ValueOf(int32(valToInt64(v)))
|
|
case reflect.Int64:
|
|
return reflect.ValueOf(valToInt64(v))
|
|
case reflect.Uint:
|
|
return reflect.ValueOf(uint(valToInt64(v)))
|
|
case reflect.Uint8:
|
|
return reflect.ValueOf(uint8(valToInt64(v)))
|
|
case reflect.Uint16:
|
|
return reflect.ValueOf(uint16(valToInt64(v)))
|
|
case reflect.Uint32:
|
|
return reflect.ValueOf(uint32(valToInt64(v)))
|
|
case reflect.Uint64:
|
|
return reflect.ValueOf(uint64(valToInt64(v)))
|
|
case reflect.Float32:
|
|
return reflect.ValueOf(float32(v.AsNumDouble()))
|
|
case reflect.Float64:
|
|
return reflect.ValueOf(v.AsNumDouble())
|
|
case reflect.Bool:
|
|
return reflect.ValueOf(v.AsBool())
|
|
case reflect.Ptr:
|
|
// Unwrap Go pointer from Value
|
|
if v.IsPointer() {
|
|
obj := v.AsPointer()
|
|
if obj != nil {
|
|
rv := reflect.ValueOf(obj)
|
|
if rv.Type().AssignableTo(targetType) {
|
|
return rv
|
|
}
|
|
}
|
|
}
|
|
return reflect.Zero(targetType)
|
|
case reflect.Slice:
|
|
if targetType.Elem().Kind() == reflect.Uint8 && v.IsString() {
|
|
// []byte from string
|
|
return reflect.ValueOf([]byte(v.AsString()))
|
|
}
|
|
if v.IsArray() {
|
|
return arrayToSlice(v, targetType)
|
|
}
|
|
return reflect.Zero(targetType)
|
|
default:
|
|
// Try interface{} unwrap
|
|
if v.IsPointer() {
|
|
obj := v.AsPointer()
|
|
if obj != nil {
|
|
rv := reflect.ValueOf(obj)
|
|
if rv.Type().AssignableTo(targetType) {
|
|
return rv
|
|
}
|
|
}
|
|
}
|
|
return reflect.Zero(targetType)
|
|
}
|
|
}
|
|
|
|
// valToInt64 safely converts any numeric Value to int64.
|
|
func valToInt64(v Value) int64 {
|
|
if v.IsInt() || v.IsLong() {
|
|
return v.AsLong()
|
|
}
|
|
// Double → truncate
|
|
return int64(v.AsNumDouble())
|
|
}
|
|
|
|
func valueToInterface(v Value) interface{} {
|
|
switch {
|
|
case v.IsNil():
|
|
return nil
|
|
case v.IsString():
|
|
return v.AsString()
|
|
case v.IsLogical():
|
|
return v.AsBool()
|
|
case v.IsDate():
|
|
return v.AsLong() // Julian
|
|
case v.IsNumeric():
|
|
if v.IsInt() {
|
|
return v.AsInt()
|
|
}
|
|
return v.AsNumDouble()
|
|
case v.IsPointer():
|
|
return v.AsPointer()
|
|
case v.IsArray():
|
|
arr := v.AsArray()
|
|
result := make([]interface{}, len(arr.Items))
|
|
for i, item := range arr.Items {
|
|
result[i] = valueToInterface(item)
|
|
}
|
|
return result
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func arrayToSlice(v Value, sliceType reflect.Type) reflect.Value {
|
|
arr := v.AsArray()
|
|
if arr == nil {
|
|
return reflect.Zero(sliceType)
|
|
}
|
|
elemType := sliceType.Elem()
|
|
slice := reflect.MakeSlice(sliceType, len(arr.Items), len(arr.Items))
|
|
for i, item := range arr.Items {
|
|
slice.Index(i).Set(valueToReflect(item, elemType))
|
|
}
|
|
return slice
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Type coercion: reflect.Value → Value
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func reflectToValue(rv reflect.Value) Value {
|
|
if !rv.IsValid() {
|
|
return MakeNil()
|
|
}
|
|
|
|
// Check error interface BEFORE unwrapping
|
|
errorType := reflect.TypeOf((*error)(nil)).Elem()
|
|
if rv.Type().Implements(errorType) {
|
|
if rv.IsNil() {
|
|
return MakeNil()
|
|
}
|
|
return MakeString(rv.Interface().(error).Error())
|
|
}
|
|
|
|
// Handle interface — unwrap
|
|
if rv.Kind() == reflect.Interface || rv.Kind() == reflect.Ptr {
|
|
if rv.IsNil() {
|
|
return MakeNil()
|
|
}
|
|
if rv.Kind() == reflect.Interface {
|
|
rv = rv.Elem()
|
|
}
|
|
}
|
|
|
|
switch rv.Kind() {
|
|
case reflect.String:
|
|
return MakeString(rv.String())
|
|
case reflect.Bool:
|
|
return MakeBool(rv.Bool())
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
n := rv.Int()
|
|
if n >= -2147483648 && n <= 2147483647 {
|
|
return MakeInt(int(n))
|
|
}
|
|
return MakeLong(n)
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return MakeLong(int64(rv.Uint()))
|
|
case reflect.Float32, reflect.Float64:
|
|
return MakeDouble(rv.Float(), 0, 0)
|
|
case reflect.Slice:
|
|
if rv.Type().Elem().Kind() == reflect.Uint8 {
|
|
// []byte → string
|
|
return MakeString(string(rv.Bytes()))
|
|
}
|
|
items := make([]Value, rv.Len())
|
|
for i := 0; i < rv.Len(); i++ {
|
|
items[i] = reflectToValue(rv.Index(i))
|
|
}
|
|
return MakeArrayFrom(items)
|
|
case reflect.Map:
|
|
h := &HbHash{}
|
|
iter := rv.MapRange()
|
|
for iter.Next() {
|
|
// Go maps guarantee unique keys; Append skips the lookup.
|
|
h.Append(reflectToValue(iter.Key()), reflectToValue(iter.Value()))
|
|
}
|
|
return MakeHashFrom(h)
|
|
case reflect.Ptr, reflect.Struct, reflect.Func, reflect.Chan:
|
|
// Wrap as Go object pointer
|
|
return WrapGo(rv.Interface())
|
|
default:
|
|
// Wrap anything else as Go object
|
|
if rv.CanInterface() {
|
|
return WrapGo(rv.Interface())
|
|
}
|
|
return MakeNil()
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GoMultiReturn — unpack Go multi-return into Harbour locals
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GoMultiAssign assigns multiple Go return values to Harbour locals.
|
|
// PRG: a, b, c := GoFunc(...)
|
|
// Generated: results := GoCallFunc(fn, args...); GoMultiAssign(t, results, 1, 2, 3)
|
|
func GoMultiAssign(t *Thread, results []Value, localIndices ...int) {
|
|
for i, idx := range localIndices {
|
|
if i < len(results) {
|
|
t.SetLocal(idx, results[i])
|
|
} else {
|
|
t.SetLocal(idx, MakeNil())
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// IsGoObject checks if a Value contains a wrapped Go object
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func IsGoObject(v Value) bool {
|
|
if !v.IsPointer() {
|
|
return false
|
|
}
|
|
obj := v.AsPointer()
|
|
if obj == nil {
|
|
return false
|
|
}
|
|
rv := reflect.ValueOf(obj)
|
|
k := rv.Kind()
|
|
return k == reflect.Ptr || k == reflect.Struct || k == reflect.Interface ||
|
|
k == reflect.Map || k == reflect.Slice || k == reflect.Chan || k == reflect.Func
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GoTypeName returns the Go type name of a wrapped object
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func GoTypeName(v Value) string {
|
|
if !v.IsPointer() {
|
|
return "Value"
|
|
}
|
|
obj := v.AsPointer()
|
|
if obj == nil {
|
|
return "nil"
|
|
}
|
|
return fmt.Sprintf("%T", obj)
|
|
}
|