// 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) } 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), } } // MakeStringBytes creates a string Value that aliases the provided byte // slice — no copy. The caller MUST guarantee the bytes remain valid and // immutable for the Value's lifetime. Use this only when the source is // stable storage (mmap region, string constant pool, builder-owned // slab). The usual caller pattern: // // - DBF reads into mmap-backed recBuf: stable until Close/Pack/Zap. // - Per-row scratch buffers: NOT safe — caller must pin. // // If you're unsure, use MakeString — the ~8-16 bytes saved per small // string isn't worth a use-after-free. func MakeStringBytes(b []byte) Value { var s string if len(b) > 0 { s = unsafe.String(&b[0], len(b)) } hs := &HbString{Data: s} return Value{ info: makeInfo(tString, 0, uint32(len(b))), 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)} }