feat: Value type methods — chain calls on String/Array/Numeric/Hash
Built-in methods on basic types, enabling fluent chaining:
cStr:Trim():Upper():Left(5) → "HELLO"
aArr:Sort():Join(",") → "1,2,3"
String (20 methods):
Upper, Lower, Trim, LTrim, RTrim, Left, Right, Substr,
Len, Replace, Split, Contains, Starts, Ends, Reverse,
Replicate, Copy, At, Empty, Val
Array (14 methods):
Len, Push, Pop, Sort, Find, Map, Filter, Each,
Join, Copy, Empty, First, Last, Slice
Numeric (6 methods):
Str, Round, Int, Abs, Sqrt, Copy
Hash (7 methods):
Keys, Values, Has, Len, Copy, Delete, Empty
Any type (5 methods):
Copy, Type, IsNil, ToStr, ClassName
Integration: Thread.Send() checks SendBuiltin() before class dispatch.
Tests: 28 tests ALL PASS including chaining test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"lastUpdated": "2026-04-01T12:09:42.812Z",
|
||||
"lastUpdated": "2026-04-02T03:31:23.903Z",
|
||||
"activeFeatures": [
|
||||
"hbrt",
|
||||
"hbrtl",
|
||||
@@ -33,9 +33,9 @@
|
||||
"documents": {},
|
||||
"timestamps": {
|
||||
"started": "2026-03-27T09:33:04.512Z",
|
||||
"lastUpdated": "2026-04-01T12:08:54.344Z"
|
||||
"lastUpdated": "2026-04-02T03:31:23.903Z"
|
||||
},
|
||||
"lastFile": "/mnt/d/charles/five/hbrt/frb.go"
|
||||
"lastFile": "/mnt/d/charles/five/hbrt/valuemethods_test.go"
|
||||
},
|
||||
"hbrtl": {
|
||||
"phase": "do",
|
||||
@@ -280,7 +280,7 @@
|
||||
"session": {
|
||||
"startedAt": "2026-03-27T06:06:49.620Z",
|
||||
"onboardingCompleted": false,
|
||||
"lastActivity": "2026-04-01T12:09:42.812Z"
|
||||
"lastActivity": "2026-04-02T03:31:23.903Z"
|
||||
},
|
||||
"history": [
|
||||
{
|
||||
@@ -5880,6 +5880,24 @@
|
||||
"feature": "five",
|
||||
"phase": "do",
|
||||
"action": "updated"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-04-02T03:29:33.962Z",
|
||||
"feature": "hbrt",
|
||||
"phase": "do",
|
||||
"action": "updated"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-04-02T03:30:25.427Z",
|
||||
"feature": "hbrt",
|
||||
"phase": "do",
|
||||
"action": "updated"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-04-02T03:31:23.903Z",
|
||||
"feature": "hbrt",
|
||||
"phase": "do",
|
||||
"action": "updated"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -225,7 +225,11 @@ func (t *Thread) Send(methodName string, nArgs int) {
|
||||
objVal := t.pop() // object
|
||||
|
||||
if !objVal.IsObject() {
|
||||
// Not an object — try as property access on non-object
|
||||
// Not a class object — try built-in Value methods (String:Upper, Array:Sort, etc.)
|
||||
if result, ok := SendBuiltin(t, objVal, methodName, args); ok {
|
||||
t.push(result)
|
||||
return
|
||||
}
|
||||
panic(t.runtimeError(fmt.Sprintf("not an object for method %s", methodName)))
|
||||
}
|
||||
|
||||
|
||||
614
hbrt/valuemethods.go
Normal file
614
hbrt/valuemethods.go
Normal file
@@ -0,0 +1,614 @@
|
||||
// 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)
|
||||
}
|
||||
key := args[0]
|
||||
for _, k := range self.AsHash().Keys {
|
||||
if valuesEqual(k, key) {
|
||||
return MakeBool(true)
|
||||
}
|
||||
}
|
||||
return MakeBool(false)
|
||||
}
|
||||
|
||||
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)
|
||||
return MakeHashFrom(nh)
|
||||
}
|
||||
|
||||
func vmHashDelete(t *Thread, self Value, args []Value) Value {
|
||||
if len(args) == 0 {
|
||||
return self
|
||||
}
|
||||
key := args[0]
|
||||
h := self.AsHash()
|
||||
for i, k := range h.Keys {
|
||||
if valuesEqual(k, key) {
|
||||
h.Keys = append(h.Keys[:i], h.Keys[i+1:]...)
|
||||
h.Values = append(h.Values[:i], h.Values[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
208
hbrt/valuemethods_test.go
Normal file
208
hbrt/valuemethods_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package hbrt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func sendMethod(t *testing.T, self Value, method string, args ...Value) Value {
|
||||
t.Helper()
|
||||
vm := NewVM()
|
||||
th := vm.NewThread()
|
||||
th.Frame(0, 0)
|
||||
result, ok := SendBuiltin(th, self, method, args)
|
||||
if !ok {
|
||||
t.Fatalf("method %s not found", method)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// === String methods ===
|
||||
|
||||
func TestVM_StrUpper(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("hello"), "Upper")
|
||||
if r.AsString() != "HELLO" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrLower(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("HELLO"), "Lower")
|
||||
if r.AsString() != "hello" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrTrim(t *testing.T) {
|
||||
r := sendMethod(t, MakeString(" hello "), "Trim")
|
||||
if r.AsString() != "hello" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrLeft(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("Hello World"), "Left", MakeInt(5))
|
||||
if r.AsString() != "Hello" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrRight(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("Hello World"), "Right", MakeInt(5))
|
||||
if r.AsString() != "World" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrReplace(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("Hello World"), "Replace", MakeString("World"), MakeString("Five"))
|
||||
if r.AsString() != "Hello Five" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrSplit(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("a,b,c"), "Split", MakeString(","))
|
||||
if !r.IsArray() { t.Fatal("not array") }
|
||||
if len(r.AsArray().Items) != 3 { t.Errorf("len=%d", len(r.AsArray().Items)) }
|
||||
if r.AsArray().Items[0].AsString() != "a" { t.Errorf("[0]=%q", r.AsArray().Items[0].AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrLen(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("Hello"), "Len")
|
||||
if r.AsInt() != 5 { t.Errorf("got %d", r.AsInt()) }
|
||||
}
|
||||
|
||||
func TestVM_StrContains(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("Hello World"), "Contains", MakeString("World"))
|
||||
if !r.AsBool() { t.Error("should be true") }
|
||||
}
|
||||
|
||||
func TestVM_StrReverse(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("Hello"), "Reverse")
|
||||
if r.AsString() != "olleH" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrCopy(t *testing.T) {
|
||||
r := sendMethod(t, MakeString("test"), "Copy")
|
||||
if r.AsString() != "test" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_StrEmpty(t *testing.T) {
|
||||
if !sendMethod(t, MakeString(""), "Empty").AsBool() { t.Error("empty string") }
|
||||
if !sendMethod(t, MakeString(" "), "Empty").AsBool() { t.Error("spaces") }
|
||||
if sendMethod(t, MakeString("x"), "Empty").AsBool() { t.Error("non-empty") }
|
||||
}
|
||||
|
||||
// === Array methods ===
|
||||
|
||||
func TestVM_ArrLen(t *testing.T) {
|
||||
arr := MakeArrayFrom([]Value{MakeInt(1), MakeInt(2), MakeInt(3)})
|
||||
r := sendMethod(t, arr, "Len")
|
||||
if r.AsInt() != 3 { t.Errorf("got %d", r.AsInt()) }
|
||||
}
|
||||
|
||||
func TestVM_ArrSort(t *testing.T) {
|
||||
arr := MakeArrayFrom([]Value{MakeInt(3), MakeInt(1), MakeInt(2)})
|
||||
r := sendMethod(t, arr, "Sort")
|
||||
items := r.AsArray().Items
|
||||
if items[0].AsInt() != 1 || items[1].AsInt() != 2 || items[2].AsInt() != 3 {
|
||||
t.Errorf("got %v %v %v", items[0].AsInt(), items[1].AsInt(), items[2].AsInt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVM_ArrFind(t *testing.T) {
|
||||
arr := MakeArrayFrom([]Value{MakeString("a"), MakeString("b"), MakeString("c")})
|
||||
r := sendMethod(t, arr, "Find", MakeString("b"))
|
||||
if r.AsInt() != 2 { t.Errorf("got %d", r.AsInt()) }
|
||||
r = sendMethod(t, arr, "Find", MakeString("z"))
|
||||
if r.AsInt() != 0 { t.Errorf("not found: got %d", r.AsInt()) }
|
||||
}
|
||||
|
||||
func TestVM_ArrJoin(t *testing.T) {
|
||||
arr := MakeArrayFrom([]Value{MakeString("a"), MakeString("b"), MakeString("c")})
|
||||
r := sendMethod(t, arr, "Join", MakeString("-"))
|
||||
if r.AsString() != "a-b-c" { t.Errorf("got %q", r.AsString()) }
|
||||
}
|
||||
|
||||
func TestVM_ArrCopy(t *testing.T) {
|
||||
arr := MakeArrayFrom([]Value{MakeInt(1), MakeInt(2)})
|
||||
cp := sendMethod(t, arr, "Copy")
|
||||
// Modify original — copy should not change
|
||||
arr.AsArray().Items[0] = MakeInt(99)
|
||||
if cp.AsArray().Items[0].AsInt() != 1 { t.Error("not a deep copy") }
|
||||
}
|
||||
|
||||
func TestVM_ArrFirstLast(t *testing.T) {
|
||||
arr := MakeArrayFrom([]Value{MakeInt(10), MakeInt(20), MakeInt(30)})
|
||||
if sendMethod(t, arr, "First").AsInt() != 10 { t.Error("first") }
|
||||
if sendMethod(t, arr, "Last").AsInt() != 30 { t.Error("last") }
|
||||
}
|
||||
|
||||
func TestVM_ArrEmpty(t *testing.T) {
|
||||
if !sendMethod(t, MakeArrayFrom(nil), "Empty").AsBool() { t.Error("empty") }
|
||||
if sendMethod(t, MakeArrayFrom([]Value{MakeInt(1)}), "Empty").AsBool() { t.Error("non-empty") }
|
||||
}
|
||||
|
||||
// === Numeric methods ===
|
||||
|
||||
func TestVM_NumRound(t *testing.T) {
|
||||
r := sendMethod(t, MakeDouble(3.14159, 0, 0), "Round", MakeInt(2))
|
||||
diff := r.AsDouble() - 3.14
|
||||
if diff > 0.001 || diff < -0.001 { t.Errorf("got %f", r.AsDouble()) }
|
||||
}
|
||||
|
||||
func TestVM_NumAbs(t *testing.T) {
|
||||
r := sendMethod(t, MakeDouble(-42.5, 0, 0), "Abs")
|
||||
if r.AsDouble() != 42.5 { t.Errorf("got %f", r.AsDouble()) }
|
||||
}
|
||||
|
||||
func TestVM_NumInt(t *testing.T) {
|
||||
r := sendMethod(t, MakeDouble(3.7, 0, 0), "Int")
|
||||
if r.AsInt() != 3 { t.Errorf("got %d", r.AsInt()) }
|
||||
}
|
||||
|
||||
// === Hash methods ===
|
||||
|
||||
func TestVM_HashKeys(t *testing.T) {
|
||||
h := MakeHash()
|
||||
hh := h.AsHash()
|
||||
hh.Keys = append(hh.Keys, MakeString("name"), MakeString("age"))
|
||||
hh.Values = append(hh.Values, MakeString("Charles"), MakeInt(30))
|
||||
|
||||
r := sendMethod(t, h, "Keys")
|
||||
if !r.IsArray() || len(r.AsArray().Items) != 2 { t.Error("keys") }
|
||||
}
|
||||
|
||||
func TestVM_HashHas(t *testing.T) {
|
||||
h := MakeHash()
|
||||
hh := h.AsHash()
|
||||
hh.Keys = append(hh.Keys, MakeString("name"))
|
||||
hh.Values = append(hh.Values, MakeString("Charles"))
|
||||
|
||||
if !sendMethod(t, h, "Has", MakeString("name")).AsBool() { t.Error("has name") }
|
||||
if sendMethod(t, h, "Has", MakeString("age")).AsBool() { t.Error("no age") }
|
||||
}
|
||||
|
||||
func TestVM_HashCopy(t *testing.T) {
|
||||
h := MakeHash()
|
||||
hh := h.AsHash()
|
||||
hh.Keys = append(hh.Keys, MakeString("k"))
|
||||
hh.Values = append(hh.Values, MakeInt(1))
|
||||
|
||||
cp := sendMethod(t, h, "Copy")
|
||||
hh.Values[0] = MakeInt(99) // modify original
|
||||
if cp.AsHash().Values[0].AsInt() != 1 { t.Error("not deep copy") }
|
||||
}
|
||||
|
||||
// === Any type methods ===
|
||||
|
||||
func TestVM_AnyType(t *testing.T) {
|
||||
if sendMethod(t, MakeString("x"), "Type").AsString() != "C" { t.Error("C") }
|
||||
if sendMethod(t, MakeInt(1), "Type").AsString() != "N" { t.Error("N") }
|
||||
if sendMethod(t, MakeBool(true), "Type").AsString() != "L" { t.Error("L") }
|
||||
if sendMethod(t, MakeNil(), "Type").AsString() != "U" { t.Error("U") }
|
||||
}
|
||||
|
||||
func TestVM_AnyToStr(t *testing.T) {
|
||||
if sendMethod(t, MakeInt(42), "ToStr").AsString() != "42" { t.Error("42") }
|
||||
if sendMethod(t, MakeBool(true), "ToStr").AsString() != ".T." { t.Error(".T.") }
|
||||
}
|
||||
|
||||
// === Chaining ===
|
||||
|
||||
func TestVM_StringChaining(t *testing.T) {
|
||||
// " Hello World " :Trim() :Upper() :Left(5) → "HELLO"
|
||||
s := MakeString(" Hello World ")
|
||||
r, _ := SendBuiltin(nil, s, "Trim", nil)
|
||||
r, _ = SendBuiltin(nil, r, "Upper", nil)
|
||||
r, _ = SendBuiltin(nil, r, "Left", []Value{MakeInt(5)})
|
||||
if r.AsString() != "HELLO" { t.Errorf("chain: got %q", r.AsString()) }
|
||||
}
|
||||
Reference in New Issue
Block a user