Files
five/hbrt/gobridge.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

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)
}