diff --git a/docs/.pdca-status.json b/docs/.pdca-status.json index 6c3b3db..340ff30 100644 --- a/docs/.pdca-status.json +++ b/docs/.pdca-status.json @@ -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" } ] } \ No newline at end of file diff --git a/hbrt/class.go b/hbrt/class.go index 66897c8..4d09499 100644 --- a/hbrt/class.go +++ b/hbrt/class.go @@ -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))) } diff --git a/hbrt/valuemethods.go b/hbrt/valuemethods.go new file mode 100644 index 0000000..84530aa --- /dev/null +++ b/hbrt/valuemethods.go @@ -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 +} diff --git a/hbrt/valuemethods_test.go b/hbrt/valuemethods_test.go new file mode 100644 index 0000000..164c0e3 --- /dev/null +++ b/hbrt/valuemethods_test.go @@ -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()) } +}