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

602 lines
14 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// valuemethods.go — Built-in methods on basic Value types.
//
// Enables method-style calls on strings, arrays, numbers, hashes:
// cStr:Upper() → strings.ToUpper
// aArr:Sort() → sort + return
// nVal:Round(2) → Round(n, dec)
// hHash:Keys() → key array
//
// Key feature: CHAINING
// cStr:Trim():Upper():Left(5)
//
// Implementation: Thread.SendBuiltin() is called before class dispatch.
// If the value is not an object but has a matching built-in method,
// execute it and return true. Otherwise fall through to class Send.
package hbrt
import (
"fmt"
"math"
"sort"
"strings"
)
// ValueMethod is a built-in method on a basic type.
type ValueMethod func(t *Thread, self Value, args []Value) Value
// builtinMethods maps "TYPE:METHOD" → implementation.
var builtinMethods = map[string]ValueMethod{
// --- String methods ---
"S:UPPER": vmStrUpper,
"S:LOWER": vmStrLower,
"S:TRIM": vmStrTrim,
"S:LTRIM": vmStrLTrim,
"S:RTRIM": vmStrRTrim,
"S:LEFT": vmStrLeft,
"S:RIGHT": vmStrRight,
"S:SUBSTR": vmStrSubstr,
"S:LEN": vmStrLen,
"S:REPLACE": vmStrReplace,
"S:SPLIT": vmStrSplit,
"S:CONTAINS": vmStrContains,
"S:STARTS": vmStrStarts,
"S:ENDS": vmStrEnds,
"S:REVERSE": vmStrReverse,
"S:REPLICATE": vmStrReplicate,
"S:COPY": vmStrCopy,
"S:AT": vmStrAt,
"S:EMPTY": vmStrEmpty,
"S:VAL": vmStrVal,
// --- Array methods ---
"A:LEN": vmArrLen,
"A:PUSH": vmArrPush,
"A:POP": vmArrPop,
"A:SORT": vmArrSort,
"A:FIND": vmArrFind,
"A:MAP": vmArrMap,
"A:FILTER": vmArrFilter,
"A:EACH": vmArrEach,
"A:JOIN": vmArrJoin,
"A:COPY": vmArrCopy,
"A:EMPTY": vmArrEmpty,
"A:FIRST": vmArrFirst,
"A:LAST": vmArrLast,
"A:SLICE": vmArrSlice,
// --- Numeric methods ---
"N:STR": vmNumStr,
"N:ROUND": vmNumRound,
"N:INT": vmNumInt,
"N:ABS": vmNumAbs,
"N:SQRT": vmNumSqrt,
"N:COPY": vmNumCopy,
// --- Hash methods ---
"H:KEYS": vmHashKeys,
"H:VALUES": vmHashValues,
"H:HAS": vmHashHas,
"H:LEN": vmHashLen,
"H:COPY": vmHashCopy,
"H:DELETE": vmHashDelete,
"H:EMPTY": vmHashEmpty,
// --- Any type ---
"*:COPY": vmAnyCopy,
"*:TYPE": vmAnyType,
"*:ISNIL": vmAnyIsNil,
"*:TOSTR": vmAnyToStr,
"*:CLASSNAME": vmAnyClassName,
}
// SendBuiltin tries to dispatch a method call on a basic type.
// Returns (result, true) if handled, (nil, false) if not.
func SendBuiltin(t *Thread, self Value, method string, args []Value) (Value, bool) {
upper := strings.ToUpper(method)
// Determine type prefix
var prefix string
switch {
case self.IsString():
prefix = "S"
case self.IsArray():
prefix = "A"
case self.IsNumeric():
prefix = "N"
case self.IsHash():
prefix = "H"
default:
prefix = "*"
}
// Try type-specific method
key := prefix + ":" + upper
if fn, ok := builtinMethods[key]; ok {
return fn(t, self, args), true
}
// Try wildcard method
key = "*:" + upper
if fn, ok := builtinMethods[key]; ok {
return fn(t, self, args), true
}
return MakeNil(), false
}
// ========= String methods =========
func vmStrUpper(t *Thread, self Value, args []Value) Value {
return MakeString(strings.ToUpper(self.AsString()))
}
func vmStrLower(t *Thread, self Value, args []Value) Value {
return MakeString(strings.ToLower(self.AsString()))
}
func vmStrTrim(t *Thread, self Value, args []Value) Value {
return MakeString(strings.TrimSpace(self.AsString()))
}
func vmStrLTrim(t *Thread, self Value, args []Value) Value {
return MakeString(strings.TrimLeft(self.AsString(), " "))
}
func vmStrRTrim(t *Thread, self Value, args []Value) Value {
return MakeString(strings.TrimRight(self.AsString(), " "))
}
func vmStrLeft(t *Thread, self Value, args []Value) Value {
s := self.AsString()
n := argInt(args, 0, len(s))
if n > len(s) {
n = len(s)
}
if n < 0 {
n = 0
}
return MakeString(s[:n])
}
func vmStrRight(t *Thread, self Value, args []Value) Value {
s := self.AsString()
n := argInt(args, 0, len(s))
if n > len(s) {
n = len(s)
}
if n < 0 {
n = 0
}
return MakeString(s[len(s)-n:])
}
func vmStrSubstr(t *Thread, self Value, args []Value) Value {
s := self.AsString()
start := argInt(args, 0, 1) - 1 // 1-based to 0-based
length := argInt(args, 1, len(s)-start)
if start < 0 {
start = 0
}
if start >= len(s) {
return MakeString("")
}
end := start + length
if end > len(s) {
end = len(s)
}
return MakeString(s[start:end])
}
func vmStrLen(t *Thread, self Value, args []Value) Value {
return MakeInt(len(self.AsString()))
}
func vmStrReplace(t *Thread, self Value, args []Value) Value {
old := argStr(args, 0, "")
new := argStr(args, 1, "")
return MakeString(strings.ReplaceAll(self.AsString(), old, new))
}
func vmStrSplit(t *Thread, self Value, args []Value) Value {
sep := argStr(args, 0, ",")
parts := strings.Split(self.AsString(), sep)
items := make([]Value, len(parts))
for i, p := range parts {
items[i] = MakeString(p)
}
return MakeArrayFrom(items)
}
func vmStrContains(t *Thread, self Value, args []Value) Value {
return MakeBool(strings.Contains(self.AsString(), argStr(args, 0, "")))
}
func vmStrStarts(t *Thread, self Value, args []Value) Value {
return MakeBool(strings.HasPrefix(self.AsString(), argStr(args, 0, "")))
}
func vmStrEnds(t *Thread, self Value, args []Value) Value {
return MakeBool(strings.HasSuffix(self.AsString(), argStr(args, 0, "")))
}
func vmStrReverse(t *Thread, self Value, args []Value) Value {
runes := []rune(self.AsString())
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return MakeString(string(runes))
}
func vmStrReplicate(t *Thread, self Value, args []Value) Value {
n := argInt(args, 0, 1)
return MakeString(strings.Repeat(self.AsString(), n))
}
func vmStrCopy(t *Thread, self Value, args []Value) Value {
s := self.AsString()
return MakeString(strings.Clone(s))
}
func vmStrAt(t *Thread, self Value, args []Value) Value {
sub := argStr(args, 0, "")
return MakeInt(strings.Index(self.AsString(), sub) + 1) // 1-based
}
func vmStrEmpty(t *Thread, self Value, args []Value) Value {
return MakeBool(len(strings.TrimSpace(self.AsString())) == 0)
}
func vmStrVal(t *Thread, self Value, args []Value) Value {
s := strings.TrimSpace(self.AsString())
if f, err := parseStrToFloat(s); err == nil {
return MakeDoubleAuto(f)
}
return MakeInt(0)
}
// ========= Array methods =========
func vmArrLen(t *Thread, self Value, args []Value) Value {
return MakeInt(len(self.AsArray().Items))
}
func vmArrPush(t *Thread, self Value, args []Value) Value {
arr := self.AsArray()
for _, a := range args {
arr.Items = append(arr.Items, a)
}
return self // return self for chaining
}
func vmArrPop(t *Thread, self Value, args []Value) Value {
arr := self.AsArray()
if len(arr.Items) == 0 {
return MakeNil()
}
last := arr.Items[len(arr.Items)-1]
arr.Items = arr.Items[:len(arr.Items)-1]
return last
}
func vmArrSort(t *Thread, self Value, args []Value) Value {
arr := self.AsArray()
items := make([]Value, len(arr.Items))
copy(items, arr.Items)
sort.SliceStable(items, func(i, j int) bool {
a, b := items[i], items[j]
if a.IsString() && b.IsString() {
return a.AsString() < b.AsString()
}
if a.IsNumeric() && b.IsNumeric() {
return a.AsNumDouble() < b.AsNumDouble()
}
return false
})
return MakeArrayFrom(items)
}
func vmArrFind(t *Thread, self Value, args []Value) Value {
if len(args) == 0 {
return MakeInt(0)
}
target := args[0]
for i, item := range self.AsArray().Items {
if valuesEqual(item, target) {
return MakeInt(i + 1) // 1-based
}
}
return MakeInt(0)
}
func vmArrMap(t *Thread, self Value, args []Value) Value {
if len(args) == 0 || !args[0].IsBlock() {
return self
}
blk := args[0].AsBlock()
arr := self.AsArray()
result := make([]Value, len(arr.Items))
for i, item := range arr.Items {
t.push(item)
t.PendingParams2(1)
blk.Fn(t)
result[i] = t.pop()
}
return MakeArrayFrom(result)
}
func vmArrFilter(t *Thread, self Value, args []Value) Value {
if len(args) == 0 || !args[0].IsBlock() {
return self
}
blk := args[0].AsBlock()
arr := self.AsArray()
var result []Value
for _, item := range arr.Items {
t.push(item)
t.PendingParams2(1)
blk.Fn(t)
keep := t.pop()
if keep.AsBool() {
result = append(result, item)
}
}
return MakeArrayFrom(result)
}
func vmArrEach(t *Thread, self Value, args []Value) Value {
if len(args) == 0 || !args[0].IsBlock() {
return self
}
blk := args[0].AsBlock()
for _, item := range self.AsArray().Items {
t.push(item)
t.PendingParams2(1)
blk.Fn(t)
t.pop() // discard result
}
return self // return self for chaining
}
func vmArrJoin(t *Thread, self Value, args []Value) Value {
sep := argStr(args, 0, ",")
arr := self.AsArray()
parts := make([]string, len(arr.Items))
for i, item := range arr.Items {
parts[i] = item.AsString()
}
return MakeString(strings.Join(parts, sep))
}
func vmArrCopy(t *Thread, self Value, args []Value) Value {
arr := self.AsArray()
items := make([]Value, len(arr.Items))
copy(items, arr.Items)
return MakeArrayFrom(items)
}
func vmArrEmpty(t *Thread, self Value, args []Value) Value {
return MakeBool(len(self.AsArray().Items) == 0)
}
func vmArrFirst(t *Thread, self Value, args []Value) Value {
arr := self.AsArray()
if len(arr.Items) > 0 {
return arr.Items[0]
}
return MakeNil()
}
func vmArrLast(t *Thread, self Value, args []Value) Value {
arr := self.AsArray()
if len(arr.Items) > 0 {
return arr.Items[len(arr.Items)-1]
}
return MakeNil()
}
func vmArrSlice(t *Thread, self Value, args []Value) Value {
arr := self.AsArray()
from := argInt(args, 0, 1) - 1 // 1-based to 0-based
to := argInt(args, 1, len(arr.Items))
if from < 0 {
from = 0
}
if to > len(arr.Items) {
to = len(arr.Items)
}
items := make([]Value, to-from)
copy(items, arr.Items[from:to])
return MakeArrayFrom(items)
}
// ========= Numeric methods =========
func vmNumStr(t *Thread, self Value, args []Value) Value {
width := argInt(args, 0, 10)
dec := argInt(args, 1, 0)
return MakeString(fmt.Sprintf("%*.*f", width, dec, self.AsNumDouble()))
}
func vmNumRound(t *Thread, self Value, args []Value) Value {
dec := argInt(args, 0, 0)
mul := math.Pow(10, float64(dec))
return MakeDoubleAuto(math.Round(self.AsNumDouble()*mul) / mul)
}
func vmNumInt(t *Thread, self Value, args []Value) Value {
return MakeInt(int(self.AsNumDouble()))
}
func vmNumAbs(t *Thread, self Value, args []Value) Value {
return MakeDoubleAuto(math.Abs(self.AsNumDouble()))
}
func vmNumSqrt(t *Thread, self Value, args []Value) Value {
return MakeDoubleAuto(math.Sqrt(self.AsNumDouble()))
}
func vmNumCopy(t *Thread, self Value, args []Value) Value {
return self // numeric values are immutable
}
// ========= Hash methods =========
func vmHashKeys(t *Thread, self Value, args []Value) Value {
h := self.AsHash()
items := make([]Value, len(h.Keys))
copy(items, h.Keys)
return MakeArrayFrom(items)
}
func vmHashValues(t *Thread, self Value, args []Value) Value {
h := self.AsHash()
items := make([]Value, len(h.Values))
copy(items, h.Values)
return MakeArrayFrom(items)
}
func vmHashHas(t *Thread, self Value, args []Value) Value {
if len(args) == 0 {
return MakeBool(false)
}
return MakeBool(self.AsHash().Has(args[0]))
}
func vmHashLen(t *Thread, self Value, args []Value) Value {
return MakeInt(len(self.AsHash().Keys))
}
func vmHashCopy(t *Thread, self Value, args []Value) Value {
h := self.AsHash()
nh := &HbHash{
Keys: make([]Value, len(h.Keys)),
Values: make([]Value, len(h.Values)),
}
copy(nh.Keys, h.Keys)
copy(nh.Values, h.Values)
// Index is rebuilt lazily on first Lookup against nh.
return MakeHashFrom(nh)
}
func vmHashDelete(t *Thread, self Value, args []Value) Value {
if len(args) == 0 {
return self
}
self.AsHash().Delete(args[0])
return self
}
func vmHashEmpty(t *Thread, self Value, args []Value) Value {
return MakeBool(len(self.AsHash().Keys) == 0)
}
// ========= Any type methods =========
func vmAnyCopy(t *Thread, self Value, args []Value) Value {
// Generic copy — delegates to type-specific
switch {
case self.IsString():
return vmStrCopy(t, self, args)
case self.IsArray():
return vmArrCopy(t, self, args)
case self.IsHash():
return vmHashCopy(t, self, args)
default:
return self // immutable types return self
}
}
func vmAnyType(t *Thread, self Value, args []Value) Value {
switch {
case self.IsNil():
return MakeString("U")
case self.IsString():
return MakeString("C")
case self.IsNumeric():
return MakeString("N")
case self.IsLogical():
return MakeString("L")
case self.IsDate():
return MakeString("D")
case self.IsArray():
return MakeString("A")
case self.IsHash():
return MakeString("H")
case self.IsBlock():
return MakeString("B")
default:
return MakeString("U")
}
}
func vmAnyIsNil(t *Thread, self Value, args []Value) Value {
return MakeBool(self.IsNil())
}
func vmAnyToStr(t *Thread, self Value, args []Value) Value {
switch {
case self.IsString():
return self
case self.IsNumeric():
return MakeString(fmt.Sprintf("%v", self.AsNumDouble()))
case self.IsLogical():
if self.AsBool() {
return MakeString(".T.")
}
return MakeString(".F.")
case self.IsNil():
return MakeString("NIL")
default:
return MakeString(fmt.Sprintf("%v", self))
}
}
func vmAnyClassName(t *Thread, self Value, args []Value) Value {
if self.IsObject() {
cls := GetClass(self.AsArray().Class)
if cls != nil {
return MakeString(cls.Name)
}
}
return MakeString("")
}
// ========= Helpers =========
func argInt(args []Value, index, def int) int {
if index < len(args) && args[index].IsNumeric() {
return args[index].AsInt()
}
return def
}
func argStr(args []Value, index int, def string) string {
if index < len(args) && args[index].IsString() {
return args[index].AsString()
}
return def
}
func valuesEqual(a, b Value) bool {
if a.IsString() && b.IsString() {
return a.AsString() == b.AsString()
}
if a.IsNumeric() && b.IsNumeric() {
return a.AsNumDouble() == b.AsNumDouble()
}
if a.IsLogical() && b.IsLogical() {
return a.AsBool() == b.AsBool()
}
return a.IsNil() && b.IsNil()
}
func parseStrToFloat(s string) (float64, error) {
var result float64
_, err := fmt.Sscanf(s, "%f", &result)
return result, err
}