// 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 }