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

534 lines
14 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Package hbrt provides the core runtime for the Five language.
// Tagged Value 24B: the fundamental value representation.
//
// Layout:
// scalar (uint64): numeric/date/bool raw bits
// info (uint64): [type:8][meta:24][aux:32]
// ptr (unsafe.Pointer): GC-traced pointer for string/array/hash/block
//
// Design rationale:
// - 24B vs Harbour's 32B HB_ITEM = 25% smaller
// - Scalar types (55% of runtime values) use scalar+info only, ptr=nil
// - Pointer types use ptr field, which Go's GC can trace directly
// - No global pointer store, no mutex, no memory leaks
// - Inspired by typescript-go (tsgo): "don't fight the GC, design around it"
//
// See docs/harbour-type-system-analysis.md for full analysis.
package hbrt
import (
"fmt"
"math"
"strconv"
"unsafe"
)
// NtoS converts int64 to string. Used by generated code for SET ORDER TO.
func NtoS(n int64) string { return strconv.FormatInt(n, 10) }
// Value is the fundamental value type in Five (24 bytes).
// Scalar types use scalar+info fields (ptr is nil).
// Pointer types use ptr field (GC-traced) + info for metadata.
type Value struct {
scalar uint64 // numeric/date/bool raw bits
info uint64 // [type:8 bits][meta:24 bits][aux:32 bits]
ptr unsafe.Pointer // GC-traced pointer (nil for scalar types)
}
// --- Type constants (upper 8 bits of info) ---
const (
tNil byte = 0
tLogical byte = 1
tInt byte = 2
tLong byte = 3
tDouble byte = 4
tDate byte = 5
tTimestamp byte = 6
tString byte = 7
tArray byte = 8
tHash byte = 9
tBlock byte = 10
tSymbol byte = 11
tByref byte = 12
tPointer byte = 13
tObject byte = 14
)
// info field bit layout
const (
typeShift = 56
metaMask = 0x00FFFFFF00000000
metaShift = 32
auxMask = 0x00000000FFFFFFFF
)
func makeInfo(typ byte, meta uint32, aux uint32) uint64 {
return uint64(typ)<<typeShift | uint64(meta&0x00FFFFFF)<<metaShift | uint64(aux)
}
// --- Type checking ---
func (v Value) Type() byte { return byte(v.info >> typeShift) }
func (v Value) IsNil() bool { return v.Type() == tNil }
func (v Value) IsLogical() bool { return v.Type() == tLogical }
func (v Value) IsInt() bool { return v.Type() == tInt }
func (v Value) IsLong() bool { return v.Type() == tLong }
func (v Value) IsDouble() bool { return v.Type() == tDouble }
func (v Value) IsDate() bool { return v.Type() == tDate }
func (v Value) IsTimestamp() bool { return v.Type() == tTimestamp }
func (v Value) IsString() bool { return v.Type() == tString }
func (v Value) IsArray() bool { t := v.Type(); return t == tArray || t == tObject }
func (v Value) IsHash() bool { return v.Type() == tHash }
func (v Value) IsBlock() bool { return v.Type() == tBlock }
func (v Value) IsSymbol() bool { return v.Type() == tSymbol }
func (v Value) IsByref() bool { return v.Type() == tByref }
func (v Value) IsPointer() bool { return v.Type() == tPointer }
func (v Value) IsObject() bool { return v.Type() == tObject }
// Composite type checks (matching Harbour's HB_IT_* groups)
func (v Value) IsNumeric() bool { t := v.Type(); return t == tInt || t == tLong || t == tDouble }
func (v Value) IsNumInt() bool { t := v.Type(); return t == tInt || t == tLong }
func (v Value) IsDateTime() bool { t := v.Type(); return t == tDate || t == tTimestamp }
// --- Scalar constructors (no heap allocation) ---
// Pre-cached common values — zero allocation for hot paths.
var (
cachedNil = Value{info: makeInfo(tNil, 0, 0)}
cachedTrue = Value{scalar: 1, info: makeInfo(tLogical, 0, 0)}
cachedFalse = Value{scalar: 0, info: makeInfo(tLogical, 0, 0)}
cachedInt0 = Value{scalar: 0, info: makeInfo(tInt, 1, 0)}
cachedInt1 = Value{scalar: 1, info: makeInfo(tInt, 1, 0)}
)
func MakeNil() Value { return cachedNil }
func MakeBool(b bool) Value {
if b {
return cachedTrue
}
return cachedFalse
}
// MakeInt creates an integer Value with display width.
// Pre-cached small integers 0-255 (avoids intExpLen loop + allocation per call).
var smallInts [256]Value
func init() {
for i := range smallInts {
smallInts[i] = Value{
scalar: uint64(int64(i)),
info: makeInfo(tInt, uint32(intExpLen(int64(i))), 0),
}
}
}
func MakeInt(v int) Value {
if v >= 0 && v < 256 {
return smallInts[v]
}
return Value{
scalar: uint64(int64(v)),
info: makeInfo(tInt, uint32(intExpLen(int64(v))), 0),
}
}
// MakeLong creates a 64-bit integer Value.
func MakeLong(v int64) Value {
return Value{
scalar: uint64(v),
info: makeInfo(tLong, uint32(longExpLen(v)), 0),
}
}
// MakeDouble creates a double Value with display width and decimal places.
func MakeDouble(v float64, length, decimal uint16) Value {
meta := uint32(length)<<8 | uint32(decimal)
return Value{
scalar: math.Float64bits(v),
info: makeInfo(tDouble, meta, 0),
}
}
// MakeDoubleAuto creates a double with default display format.
func MakeDoubleAuto(v float64) Value {
return MakeDouble(v, 255, 255)
}
// MakeDate creates a date Value from Julian day number.
func MakeDate(julian int64) Value {
return Value{scalar: uint64(julian), info: makeInfo(tDate, 0, 0)}
}
// MakeTimestamp creates a timestamp Value from Julian day + milliseconds.
func MakeTimestamp(julian int64, timeMs int32) Value {
return Value{
scalar: uint64(julian),
info: makeInfo(tTimestamp, 0, uint32(timeMs)),
}
}
// --- Value extraction ---
func (v Value) AsBool() bool { return v.scalar != 0 }
func (v Value) AsInt() int { return int(int64(v.scalar)) }
func (v Value) AsLong() int64 { return int64(v.scalar) }
func (v Value) AsDouble() float64 { return math.Float64frombits(v.scalar) }
func (v Value) AsJulian() int64 { return int64(v.scalar) }
func (v Value) AsTimeMs() int32 { return int32(v.info & auxMask) }
func (v Value) AsNumInt() int64 {
if v.Type() == tDouble {
return int64(math.Float64frombits(v.scalar))
}
return int64(v.scalar)
}
// AsNumDouble returns a double value from any numeric type.
func (v Value) AsNumDouble() float64 {
switch v.Type() {
case tInt, tLong:
return float64(int64(v.scalar))
case tDouble:
return math.Float64frombits(v.scalar)
default:
return 0
}
}
// Display metadata
func (v Value) Length() uint16 {
switch v.Type() {
case tInt, tLong:
return uint16((v.info & metaMask) >> metaShift)
case tDouble:
return uint16((v.info & metaMask) >> (metaShift + 8))
default:
return 0
}
}
func (v Value) Decimal() uint16 {
if v.Type() == tDouble {
return uint16((v.info & metaMask) >> metaShift & 0xFF)
}
return 0
}
// --- Pointer type backing stores ---
// HbString is the string backing store.
type HbString struct {
Data string // Go immutable string (primary storage)
Bytes []byte // mutable buffer (for in-place edits, nil if immutable)
}
// HbArray is the array/object backing store.
type HbArray struct {
Items []Value
Class uint16
PrevCls uint16
}
// HbHash is the hash table backing store.
//
// Keys/Values are parallel slices kept in insertion order (Harbour
// HB_HASH_KEEPORDER default). Index is an O(1) lookup map mirroring
// entries whose key type is indexable (string, numeric, logical, nil);
// keys of other types fall back to a linear scan through Keys.
//
// Callers that mutate Keys/Values directly (tests, bulk loaders) may
// leave Index stale — the helper methods detect that via a length
// mismatch and rebuild on demand. Production code must go through the
// Lookup/Set/Append/Delete methods to keep Index in sync.
type HbHash struct {
Keys []Value
Values []Value
Order []int
Flags int32
Index map[string]int
}
// HbBlock is the code block backing store.
type HbBlock struct {
Fn func(*Thread)
DetachedLen int
Detached []Value
}
// --- Pointer type constructors ---
// These store Go pointers in Value.ptr, which the GC can trace.
// No global store, no mutex, no memory leaks.
// MakeString creates a string Value.
func MakeString(s string) Value {
hs := &HbString{Data: s}
return Value{
info: makeInfo(tString, 0, uint32(len(s))),
ptr: unsafe.Pointer(hs),
}
}
// MakeArray creates an array Value.
func MakeArray(size int) Value {
ha := &HbArray{Items: make([]Value, size)}
return Value{
info: makeInfo(tArray, 0, 0),
ptr: unsafe.Pointer(ha),
}
}
// MakeArrayFrom creates an array Value from existing items.
func MakeArrayFrom(items []Value) Value {
ha := &HbArray{Items: items}
return Value{
info: makeInfo(tArray, 0, 0),
ptr: unsafe.Pointer(ha),
}
}
// ArraySlab is a pre-allocated pool of HbArray headers. SQL scan loops
// create one array per matching row; allocating them one at a time
// generates O(n) heap traffic. This lets the caller allocate all the
// headers in a single backing slice and hand out stable pointers.
//
// Usage:
// slab := hbrt.NewArraySlab(estRows)
// for each row:
// items := flat[off:end:end]
// rows = append(rows, slab.WrapNext(items))
//
// Each WrapNext advances the slab cursor. If the slab overflows, a new
// backing slice is allocated transparently; pointers already handed out
// remain valid because they reference fixed addresses in the old slice.
type ArraySlab struct {
buf []HbArray
idx int
}
// NewArraySlab returns an ArraySlab pre-sized for n rows.
func NewArraySlab(n int) *ArraySlab {
if n < 16 {
n = 16
}
return &ArraySlab{buf: make([]HbArray, n)}
}
// WrapNext stores items into the next free HbArray slot and returns
// a Value wrapping it. Grows the slab if exhausted.
func (s *ArraySlab) WrapNext(items []Value) Value {
if s.idx >= len(s.buf) {
// Exhausted — allocate a fresh slab. Old slab stays alive because
// previously handed-out pointers reference elements inside it.
newSize := len(s.buf) * 2
s.buf = make([]HbArray, newSize)
s.idx = 0
}
ha := &s.buf[s.idx]
ha.Items = items
s.idx++
return Value{
info: makeInfo(tArray, 0, 0),
ptr: unsafe.Pointer(ha),
}
}
// MakeObject creates an object Value (array with class).
func MakeObject(classID uint16, fieldCount int) Value {
ha := &HbArray{Items: make([]Value, fieldCount), Class: classID}
return Value{
info: makeInfo(tObject, uint32(classID), 0),
ptr: unsafe.Pointer(ha),
}
}
// MakeHash creates an empty hash Value.
func MakeHash() Value {
hh := &HbHash{}
return Value{
info: makeInfo(tHash, 0, 0),
ptr: unsafe.Pointer(hh),
}
}
func MakeHashFrom(hh *HbHash) Value {
return Value{
info: makeInfo(tHash, 0, 0),
ptr: unsafe.Pointer(hh),
}
}
// MakeBlock creates a code block Value.
func MakeBlock(fn func(*Thread), detachedLocals int) Value {
hb := &HbBlock{
Fn: fn,
DetachedLen: detachedLocals,
Detached: make([]Value, detachedLocals),
}
return Value{
info: makeInfo(tBlock, 0, 0),
ptr: unsafe.Pointer(hb),
}
}
// --- Pointer type accessors ---
func (v Value) AsString() string {
if v.ptr == nil {
return ""
}
hs := (*HbString)(v.ptr)
if hs.Bytes != nil {
return string(hs.Bytes)
}
return hs.Data
}
func (v Value) StringLen() int {
return int(v.info & auxMask)
}
func (v Value) AsArray() *HbArray {
if v.ptr == nil {
return nil
}
return (*HbArray)(v.ptr)
}
func (v Value) AsHash() *HbHash {
if v.ptr == nil {
return nil
}
return (*HbHash)(v.ptr)
}
func (v Value) AsBlock() *HbBlock {
if v.ptr == nil {
return nil
}
return (*HbBlock)(v.ptr)
}
// AsPointer returns the Go interface{} stored in a Pointer value.
func (v Value) AsPointer() interface{} {
if v.ptr == nil {
return nil
}
return *(*interface{})(v.ptr)
}
// MakePointer wraps an arbitrary Go value as a Harbour Pointer type.
func MakePointer(val interface{}) Value {
p := new(interface{})
*p = val
return Value{
info: makeInfo(tPointer, 0, 0),
ptr: unsafe.Pointer(p),
}
}
// --- Numeric auto-promotion ---
// MakeNumInt creates an Int or Long depending on value range.
func MakeNumInt(v int64) Value {
if v >= math.MinInt32 && v <= math.MaxInt32 {
return MakeInt(int(v))
}
return MakeLong(v)
}
// --- Display length helpers ---
func intExpLen(v int64) int {
if v == 0 {
return 1
}
n := 0
if v < 0 {
n = 1
if v == math.MinInt64 {
return 20 // "-9223372036854775808"
}
v = -v
}
for v > 0 {
n++
v /= 10
}
return n
}
func longExpLen(v int64) int {
return intExpLen(v)
}
// --- Stringer ---
func (v Value) String() string {
switch v.Type() {
case tNil:
return "NIL"
case tLogical:
if v.AsBool() {
return ".T."
}
return ".F."
case tInt:
return fmt.Sprintf("%d", v.AsInt())
case tLong:
return fmt.Sprintf("%d", v.AsLong())
case tDouble:
return fmt.Sprintf("%g", v.AsDouble())
case tDate:
return fmt.Sprintf("Date(%d)", v.AsJulian())
case tTimestamp:
return fmt.Sprintf("Timestamp(%d,%d)", v.AsJulian(), v.AsTimeMs())
case tString:
return fmt.Sprintf("%q", v.AsString())
case tArray:
arr := v.AsArray()
if arr == nil {
return "Array(nil)"
}
return fmt.Sprintf("Array(%d)", len(arr.Items))
case tObject:
arr := v.AsArray()
if arr == nil {
return "Object(nil)"
}
return fmt.Sprintf("Object(class=%d, fields=%d)", arr.Class, len(arr.Items))
case tHash:
hh := v.AsHash()
if hh == nil {
return "Hash(nil)"
}
return fmt.Sprintf("Hash(%d)", len(hh.Keys))
case tBlock:
return "Block{...}"
case tSymbol:
return "Symbol"
case tByref:
if cell := (*HbRefCell)(v.ptr); cell != nil {
return fmt.Sprintf("Byref→%s", cell.V.String())
}
return "Byref(nil)"
case tPointer:
return fmt.Sprintf("Pointer(%x)", v.scalar)
default:
return fmt.Sprintf("Unknown(type=%d)", v.Type())
}
}
// --- Byref (pass-by-reference) support ---
// HbRefCell is a shared mutable cell for @variable pass-by-reference.
type HbRefCell struct{ V Value }
// MakeByref wraps a RefCell into a tByref Value.
func MakeByref(cell *HbRefCell) Value {
return Value{info: uint64(tByref) << typeShift, ptr: unsafe.Pointer(cell)}
}