From 69ef0a9d20148ad8deedb000f8ddbcb55f6e7c6b Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 21:51:01 -0500 Subject: [PATCH 01/99] Split execution type and value helpers into focused files --- vibes/execution.go | 813 -------------------------------------- vibes/execution_types.go | 421 ++++++++++++++++++++ vibes/execution_values.go | 410 +++++++++++++++++++ 3 files changed, 831 insertions(+), 813 deletions(-) create mode 100644 vibes/execution_types.go create mode 100644 vibes/execution_values.go diff --git a/vibes/execution.go b/vibes/execution.go index 3f966a7..aa9b652 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -3978,418 +3978,6 @@ func (exec *Execution) bindFunctionArgs(fn *ScriptFunction, env *Env, args []Val return nil } -func checkValueType(val Value, ty *TypeExpr) error { - matches, err := valueMatchesType(val, ty) - if err != nil { - return err - } - if matches { - return nil - } - return &typeMismatchError{ - Expected: formatTypeExpr(ty), - Actual: formatValueTypeExpr(val), - } -} - -type typeMismatchError struct { - Expected string - Actual string -} - -func (e *typeMismatchError) Error() string { - return fmt.Sprintf("expected %s, got %s", e.Expected, e.Actual) -} - -func formatArgumentTypeMismatch(name string, err error) string { - var mismatch *typeMismatchError - if errors.As(err, &mismatch) { - return fmt.Sprintf("argument %s expected %s, got %s", name, mismatch.Expected, mismatch.Actual) - } - return fmt.Sprintf("argument %s type check failed: %s", name, err.Error()) -} - -func formatReturnTypeMismatch(fnName string, err error) string { - var mismatch *typeMismatchError - if errors.As(err, &mismatch) { - return fmt.Sprintf("return value for %s expected %s, got %s", fnName, mismatch.Expected, mismatch.Actual) - } - return fmt.Sprintf("return type check failed for %s: %s", fnName, err.Error()) -} - -type typeValidationVisit struct { - valueKind ValueKind - valueID uintptr - ty *TypeExpr -} - -type typeValidationState struct { - active map[typeValidationVisit]struct{} -} - -func valueMatchesType(val Value, ty *TypeExpr) (bool, error) { - state := typeValidationState{ - active: make(map[typeValidationVisit]struct{}), - } - return state.matches(val, ty) -} - -func (s *typeValidationState) matches(val Value, ty *TypeExpr) (bool, error) { - if visit, ok := typeValidationVisitFor(val, ty); ok { - if _, seen := s.active[visit]; seen { - // Recursive value/type pair already being validated higher in the stack. - return true, nil - } - s.active[visit] = struct{}{} - defer delete(s.active, visit) - } - - if ty.Nullable && val.Kind() == KindNil { - return true, nil - } - switch ty.Kind { - case TypeAny: - return true, nil - case TypeInt: - return val.Kind() == KindInt, nil - case TypeFloat: - return val.Kind() == KindFloat, nil - case TypeNumber: - return val.Kind() == KindInt || val.Kind() == KindFloat, nil - case TypeString: - return val.Kind() == KindString, nil - case TypeBool: - return val.Kind() == KindBool, nil - case TypeNil: - return val.Kind() == KindNil, nil - case TypeDuration: - return val.Kind() == KindDuration, nil - case TypeTime: - return val.Kind() == KindTime, nil - case TypeMoney: - return val.Kind() == KindMoney, nil - case TypeArray: - if val.Kind() != KindArray { - return false, nil - } - if len(ty.TypeArgs) == 0 { - return true, nil - } - if len(ty.TypeArgs) != 1 { - return false, fmt.Errorf("array type expects exactly 1 type argument") - } - elemType := ty.TypeArgs[0] - for _, elem := range val.Array() { - matches, err := s.matches(elem, elemType) - if err != nil { - return false, err - } - if !matches { - return false, nil - } - } - return true, nil - case TypeHash: - if val.Kind() != KindHash && val.Kind() != KindObject { - return false, nil - } - if len(ty.TypeArgs) == 0 { - return true, nil - } - if len(ty.TypeArgs) != 2 { - return false, fmt.Errorf("hash type expects exactly 2 type arguments") - } - keyType := ty.TypeArgs[0] - valueType := ty.TypeArgs[1] - for key, value := range val.Hash() { - keyMatches, err := s.matches(NewString(key), keyType) - if err != nil { - return false, err - } - if !keyMatches { - return false, nil - } - valueMatches, err := s.matches(value, valueType) - if err != nil { - return false, err - } - if !valueMatches { - return false, nil - } - } - return true, nil - case TypeFunction: - return val.Kind() == KindFunction, nil - case TypeShape: - if val.Kind() != KindHash && val.Kind() != KindObject { - return false, nil - } - entries := val.Hash() - if len(ty.Shape) == 0 { - return len(entries) == 0, nil - } - for field, fieldType := range ty.Shape { - fieldVal, ok := entries[field] - if !ok { - return false, nil - } - matches, err := s.matches(fieldVal, fieldType) - if err != nil { - return false, err - } - if !matches { - return false, nil - } - } - for field := range entries { - if _, ok := ty.Shape[field]; !ok { - return false, nil - } - } - return true, nil - case TypeUnion: - for _, option := range ty.Union { - matches, err := s.matches(val, option) - if err != nil { - return false, err - } - if matches { - return true, nil - } - } - return false, nil - default: - return false, fmt.Errorf("unknown type %s", ty.Name) - } -} - -func typeValidationVisitFor(val Value, ty *TypeExpr) (typeValidationVisit, bool) { - if ty == nil { - return typeValidationVisit{}, false - } - - var valueID uintptr - switch val.Kind() { - case KindArray: - valueID = reflect.ValueOf(val.Array()).Pointer() - case KindHash, KindObject: - valueID = reflect.ValueOf(val.Hash()).Pointer() - default: - return typeValidationVisit{}, false - } - if valueID == 0 { - return typeValidationVisit{}, false - } - - return typeValidationVisit{ - valueKind: val.Kind(), - valueID: valueID, - ty: ty, - }, true -} - -func formatTypeExpr(ty *TypeExpr) string { - if ty == nil { - return "unknown" - } - - if ty.Kind == TypeUnion { - if len(ty.Union) == 0 { - return "unknown" - } - parts := make([]string, len(ty.Union)) - for i, option := range ty.Union { - parts[i] = formatTypeExpr(option) - } - return strings.Join(parts, " | ") - } - - var name string - switch ty.Kind { - case TypeAny: - name = "any" - case TypeInt: - name = "int" - case TypeFloat: - name = "float" - case TypeNumber: - name = "number" - case TypeString: - name = "string" - case TypeBool: - name = "bool" - case TypeNil: - name = "nil" - case TypeDuration: - name = "duration" - case TypeTime: - name = "time" - case TypeMoney: - name = "money" - case TypeArray: - name = "array" - case TypeHash: - name = "hash" - case TypeFunction: - name = "function" - case TypeShape: - name = formatShapeType(ty) - default: - name = ty.Name - } - if name == "" { - name = "unknown" - } - if len(ty.TypeArgs) > 0 { - args := make([]string, len(ty.TypeArgs)) - for i, typeArg := range ty.TypeArgs { - args[i] = formatTypeExpr(typeArg) - } - name = fmt.Sprintf("%s<%s>", name, strings.Join(args, ", ")) - } - if ty.Nullable && !strings.HasSuffix(name, "?") { - return name + "?" - } - return name -} - -func formatShapeType(ty *TypeExpr) string { - if ty == nil || len(ty.Shape) == 0 { - return "{}" - } - fields := make([]string, 0, len(ty.Shape)) - for field := range ty.Shape { - fields = append(fields, field) - } - sort.Strings(fields) - parts := make([]string, len(fields)) - for i, field := range fields { - parts[i] = fmt.Sprintf("%s: %s", field, formatTypeExpr(ty.Shape[field])) - } - return "{ " + strings.Join(parts, ", ") + " }" -} - -func formatValueTypeExpr(val Value) string { - state := valueTypeFormatState{ - seenArrays: make(map[uintptr]struct{}), - seenHashes: make(map[uintptr]struct{}), - } - return state.format(val) -} - -type valueTypeFormatState struct { - seenArrays map[uintptr]struct{} - seenHashes map[uintptr]struct{} -} - -func (s *valueTypeFormatState) format(val Value) string { - switch val.Kind() { - case KindNil: - return "nil" - case KindBool: - return "bool" - case KindInt: - return "int" - case KindFloat: - return "float" - case KindString: - return "string" - case KindMoney: - return "money" - case KindDuration: - return "duration" - case KindTime: - return "time" - case KindSymbol: - return "symbol" - case KindRange: - return "range" - case KindFunction: - return "function" - case KindBuiltin: - return "builtin" - case KindBlock: - return "block" - case KindClass: - return "class" - case KindInstance: - return "instance" - case KindArray: - return s.formatArray(val.Array()) - case KindHash, KindObject: - return s.formatHash(val.Hash()) - default: - return val.Kind().String() - } -} - -func (s *valueTypeFormatState) formatArray(values []Value) string { - if len(values) == 0 { - return "array" - } - - id := reflect.ValueOf(values).Pointer() - if id != 0 { - if _, seen := s.seenArrays[id]; seen { - return "array<...>" - } - s.seenArrays[id] = struct{}{} - defer delete(s.seenArrays, id) - } - - elementTypes := make(map[string]struct{}, len(values)) - for _, value := range values { - elementTypes[s.format(value)] = struct{}{} - } - return "array<" + joinSortedTypes(elementTypes) + ">" -} - -func (s *valueTypeFormatState) formatHash(values map[string]Value) string { - if len(values) == 0 { - return "{}" - } - - id := reflect.ValueOf(values).Pointer() - if id != 0 { - if _, seen := s.seenHashes[id]; seen { - return "{ ... }" - } - s.seenHashes[id] = struct{}{} - defer delete(s.seenHashes, id) - } - - if len(values) <= 6 { - fields := make([]string, 0, len(values)) - for field := range values { - fields = append(fields, field) - } - sort.Strings(fields) - parts := make([]string, len(fields)) - for i, field := range fields { - parts[i] = fmt.Sprintf("%s: %s", field, s.format(values[field])) - } - return "{ " + strings.Join(parts, ", ") + " }" - } - - valueTypes := make(map[string]struct{}, len(values)) - for _, value := range values { - valueTypes[s.format(value)] = struct{}{} - } - return "hash" -} - -func joinSortedTypes(typeSet map[string]struct{}) string { - if len(typeSet) == 0 { - return "empty" - } - parts := make([]string, 0, len(typeSet)) - for typeName := range typeSet { - parts = append(parts, typeName) - } - sort.Strings(parts) - return strings.Join(parts, " | ") -} - // Function looks up a compiled function by name. func (s *Script) Function(name string) (*ScriptFunction, bool) { fn, ok := s.functions[name] @@ -4622,404 +4210,3 @@ func (s *Script) Call(ctx context.Context, name string, args []Value, opts CallO } return val, nil } - -func valueToHashKey(val Value) (string, error) { - switch val.Kind() { - case KindSymbol: - return val.String(), nil - case KindString: - return val.String(), nil - default: - return "", fmt.Errorf("unsupported hash key type %v", val.Kind()) - } -} - -func valueToInt64(val Value) (int64, error) { - switch val.Kind() { - case KindInt: - return val.Int(), nil - case KindFloat: - return int64(val.Float()), nil - default: - return 0, fmt.Errorf("expected integer value") - } -} - -func valueToInt(val Value) (int, error) { - switch val.Kind() { - case KindInt: - return int(val.Int()), nil - case KindFloat: - return int(val.Float()), nil - default: - return 0, fmt.Errorf("expected integer index") - } -} - -func sortComparisonResult(val Value) (int, error) { - switch val.Kind() { - case KindInt: - switch { - case val.Int() < 0: - return -1, nil - case val.Int() > 0: - return 1, nil - default: - return 0, nil - } - case KindFloat: - switch { - case val.Float() < 0: - return -1, nil - case val.Float() > 0: - return 1, nil - default: - return 0, nil - } - default: - return 0, fmt.Errorf("comparator must be numeric") - } -} - -func arraySortCompareValues(left, right Value) (int, error) { - switch { - case left.Kind() == KindInt && right.Kind() == KindInt: - switch { - case left.Int() < right.Int(): - return -1, nil - case left.Int() > right.Int(): - return 1, nil - default: - return 0, nil - } - case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): - switch { - case left.Float() < right.Float(): - return -1, nil - case left.Float() > right.Float(): - return 1, nil - default: - return 0, nil - } - case left.Kind() == KindString && right.Kind() == KindString: - switch { - case left.String() < right.String(): - return -1, nil - case left.String() > right.String(): - return 1, nil - default: - return 0, nil - } - case left.Kind() == KindSymbol && right.Kind() == KindSymbol: - switch { - case left.String() < right.String(): - return -1, nil - case left.String() > right.String(): - return 1, nil - default: - return 0, nil - } - case left.Kind() == KindBool && right.Kind() == KindBool: - switch { - case !left.Bool() && right.Bool(): - return -1, nil - case left.Bool() && !right.Bool(): - return 1, nil - default: - return 0, nil - } - case left.Kind() == KindDuration && right.Kind() == KindDuration: - switch { - case left.Duration().Seconds() < right.Duration().Seconds(): - return -1, nil - case left.Duration().Seconds() > right.Duration().Seconds(): - return 1, nil - default: - return 0, nil - } - case left.Kind() == KindTime && right.Kind() == KindTime: - switch { - case left.Time().Before(right.Time()): - return -1, nil - case left.Time().After(right.Time()): - return 1, nil - default: - return 0, nil - } - case left.Kind() == KindMoney && right.Kind() == KindMoney: - if left.Money().Currency() != right.Money().Currency() { - return 0, fmt.Errorf("money values with different currencies") - } - switch { - case left.Money().Cents() < right.Money().Cents(): - return -1, nil - case left.Money().Cents() > right.Money().Cents(): - return 1, nil - default: - return 0, nil - } - case left.Kind() == KindNil && right.Kind() == KindNil: - return 0, nil - default: - return 0, fmt.Errorf("values are not comparable") - } -} - -// flattenValues recursively flattens nested arrays up to the specified depth. -// depth=-1 means flatten completely (no limit). -// depth=0 means don't flatten at all. -// depth=1 means flatten one level, etc. -func flattenValues(values []Value, depth int) []Value { - out := make([]Value, 0, len(values)) - for _, v := range values { - if v.Kind() == KindArray && depth != 0 { - nextDepth := depth - if nextDepth > 0 { - nextDepth-- - } - out = append(out, flattenValues(v.Array(), nextDepth)...) - } else { - out = append(out, v) - } - } - return out -} - -func floatToInt64Checked(v float64, method string) (int64, error) { - if math.IsNaN(v) || math.IsInf(v, 0) { - return 0, fmt.Errorf("%s result out of int64 range", method) - } - // float64(math.MaxInt64) rounds to 2^63, so use >= 2^63 as the true upper bound. - if v < float64(math.MinInt64) || v >= math.Exp2(63) { - return 0, fmt.Errorf("%s result out of int64 range", method) - } - return int64(v), nil -} - -func addValues(left, right Value) (Value, error) { - switch { - case left.Kind() == KindInt && right.Kind() == KindInt: - return NewInt(left.Int() + right.Int()), nil - case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): - return NewFloat(left.Float() + right.Float()), nil - case left.Kind() == KindTime && right.Kind() == KindDuration: - return NewTime(left.Time().Add(time.Duration(right.Duration().Seconds()) * time.Second)), nil - case right.Kind() == KindTime && left.Kind() == KindDuration: - return NewTime(right.Time().Add(time.Duration(left.Duration().Seconds()) * time.Second)), nil - case left.Kind() == KindDuration && right.Kind() == KindDuration: - return NewDuration(Duration{seconds: left.Duration().Seconds() + right.Duration().Seconds()}), nil - case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): - secs, err := valueToInt64(right) - if err != nil { - return NewNil(), fmt.Errorf("unsupported addition operands") - } - return NewDuration(Duration{seconds: left.Duration().Seconds() + secs}), nil - case right.Kind() == KindDuration && (left.Kind() == KindInt || left.Kind() == KindFloat): - secs, err := valueToInt64(left) - if err != nil { - return NewNil(), fmt.Errorf("unsupported addition operands") - } - return NewDuration(Duration{seconds: right.Duration().Seconds() + secs}), nil - case left.Kind() == KindArray && right.Kind() == KindArray: - lArr := left.Array() - rArr := right.Array() - out := make([]Value, len(lArr)+len(rArr)) - copy(out, lArr) - copy(out[len(lArr):], rArr) - return NewArray(out), nil - case left.Kind() == KindString || right.Kind() == KindString: - return NewString(left.String() + right.String()), nil - case left.Kind() == KindMoney && right.Kind() == KindMoney: - sum, err := left.Money().add(right.Money()) - if err != nil { - return NewNil(), err - } - return NewMoney(sum), nil - default: - return NewNil(), fmt.Errorf("unsupported addition operands") - } -} - -func subtractValues(left, right Value) (Value, error) { - switch { - case left.Kind() == KindInt && right.Kind() == KindInt: - return NewInt(left.Int() - right.Int()), nil - case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): - return NewFloat(left.Float() - right.Float()), nil - case left.Kind() == KindTime && right.Kind() == KindDuration: - return NewTime(left.Time().Add(-time.Duration(right.Duration().Seconds()) * time.Second)), nil - case left.Kind() == KindTime && right.Kind() == KindTime: - diff := left.Time().Sub(right.Time()) - return NewDuration(Duration{seconds: int64(diff / time.Second)}), nil - case left.Kind() == KindDuration && right.Kind() == KindDuration: - return NewDuration(Duration{seconds: left.Duration().Seconds() - right.Duration().Seconds()}), nil - case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): - secs, err := valueToInt64(right) - if err != nil { - return NewNil(), fmt.Errorf("unsupported subtraction operands") - } - return NewDuration(Duration{seconds: left.Duration().Seconds() - secs}), nil - case left.Kind() == KindArray && right.Kind() == KindArray: - lArr := left.Array() - rArr := right.Array() - out := make([]Value, 0, len(lArr)) - for _, item := range lArr { - found := slices.ContainsFunc(rArr, item.Equal) - if !found { - out = append(out, item) - } - } - return NewArray(out), nil - case left.Kind() == KindMoney && right.Kind() == KindMoney: - diff, err := left.Money().sub(right.Money()) - if err != nil { - return NewNil(), err - } - return NewMoney(diff), nil - default: - return NewNil(), fmt.Errorf("unsupported subtraction operands") - } -} - -func multiplyValues(left, right Value) (Value, error) { - switch { - case left.Kind() == KindInt && right.Kind() == KindInt: - return NewInt(left.Int() * right.Int()), nil - case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): - return NewFloat(left.Float() * right.Float()), nil - case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): - secs, err := valueToInt64(right) - if err != nil { - return NewNil(), fmt.Errorf("unsupported multiplication operands") - } - return NewDuration(Duration{seconds: left.Duration().Seconds() * secs}), nil - case right.Kind() == KindDuration && (left.Kind() == KindInt || left.Kind() == KindFloat): - secs, err := valueToInt64(left) - if err != nil { - return NewNil(), fmt.Errorf("unsupported multiplication operands") - } - return NewDuration(Duration{seconds: right.Duration().Seconds() * secs}), nil - case left.Kind() == KindMoney && right.Kind() == KindInt: - return NewMoney(left.Money().mulInt(right.Int())), nil - case left.Kind() == KindInt && right.Kind() == KindMoney: - return NewMoney(right.Money().mulInt(left.Int())), nil - default: - return NewNil(), fmt.Errorf("unsupported multiplication operands") - } -} - -func divideValues(left, right Value) (Value, error) { - switch { - case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): - if right.Float() == 0 { - return NewNil(), errors.New("division by zero") - } - return NewFloat(left.Float() / right.Float()), nil - case left.Kind() == KindDuration && right.Kind() == KindDuration: - if right.Duration().Seconds() == 0 { - return NewNil(), errors.New("division by zero") - } - return NewFloat(float64(left.Duration().Seconds()) / float64(right.Duration().Seconds())), nil - case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): - secs, err := valueToInt64(right) - if err != nil { - return NewNil(), fmt.Errorf("unsupported division operands") - } - if secs == 0 { - return NewNil(), errors.New("division by zero") - } - return NewDuration(Duration{seconds: left.Duration().Seconds() / secs}), nil - case left.Kind() == KindMoney && right.Kind() == KindInt: - res, err := left.Money().divInt(right.Int()) - if err != nil { - return NewNil(), err - } - return NewMoney(res), nil - default: - return NewNil(), fmt.Errorf("unsupported division operands") - } -} - -func moduloValues(left, right Value) (Value, error) { - if left.Kind() == KindInt && right.Kind() == KindInt { - if right.Int() == 0 { - return NewNil(), errors.New("modulo by zero") - } - return NewInt(left.Int() % right.Int()), nil - } - if left.Kind() == KindDuration && right.Kind() == KindDuration { - if right.Duration().Seconds() == 0 { - return NewNil(), errors.New("modulo by zero") - } - return NewDuration(Duration{seconds: left.Duration().Seconds() % right.Duration().Seconds()}), nil - } - return NewNil(), fmt.Errorf("unsupported modulo operands") -} - -func compareValues(expr *BinaryExpr, left, right Value, cmp func(int) bool) (Value, error) { - switch { - case left.Kind() == KindInt && right.Kind() == KindInt: - diff := left.Int() - right.Int() - switch { - case diff < 0: - return NewBool(cmp(-1)), nil - case diff > 0: - return NewBool(cmp(1)), nil - default: - return NewBool(cmp(0)), nil - } - case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): - lf, rf := left.Float(), right.Float() - switch { - case lf < rf: - return NewBool(cmp(-1)), nil - case lf > rf: - return NewBool(cmp(1)), nil - default: - return NewBool(cmp(0)), nil - } - case left.Kind() == KindString && right.Kind() == KindString: - switch { - case left.String() < right.String(): - return NewBool(cmp(-1)), nil - case left.String() > right.String(): - return NewBool(cmp(1)), nil - default: - return NewBool(cmp(0)), nil - } - case left.Kind() == KindMoney && right.Kind() == KindMoney: - if left.Money().Currency() != right.Money().Currency() { - return NewNil(), fmt.Errorf("money currency mismatch for comparison") - } - diff := left.Money().Cents() - right.Money().Cents() - switch { - case diff < 0: - return NewBool(cmp(-1)), nil - case diff > 0: - return NewBool(cmp(1)), nil - default: - return NewBool(cmp(0)), nil - } - case left.Kind() == KindDuration && right.Kind() == KindDuration: - diff := left.Duration().Seconds() - right.Duration().Seconds() - switch { - case diff < 0: - return NewBool(cmp(-1)), nil - case diff > 0: - return NewBool(cmp(1)), nil - default: - return NewBool(cmp(0)), nil - } - case left.Kind() == KindTime && right.Kind() == KindTime: - switch { - case left.Time().Before(right.Time()): - return NewBool(cmp(-1)), nil - case left.Time().After(right.Time()): - return NewBool(cmp(1)), nil - default: - return NewBool(cmp(0)), nil - } - default: - return NewNil(), fmt.Errorf("unsupported comparison operands") - } -} diff --git a/vibes/execution_types.go b/vibes/execution_types.go new file mode 100644 index 0000000..226256a --- /dev/null +++ b/vibes/execution_types.go @@ -0,0 +1,421 @@ +package vibes + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strings" +) + +func checkValueType(val Value, ty *TypeExpr) error { + matches, err := valueMatchesType(val, ty) + if err != nil { + return err + } + if matches { + return nil + } + return &typeMismatchError{ + Expected: formatTypeExpr(ty), + Actual: formatValueTypeExpr(val), + } +} + +type typeMismatchError struct { + Expected string + Actual string +} + +func (e *typeMismatchError) Error() string { + return fmt.Sprintf("expected %s, got %s", e.Expected, e.Actual) +} + +func formatArgumentTypeMismatch(name string, err error) string { + var mismatch *typeMismatchError + if errors.As(err, &mismatch) { + return fmt.Sprintf("argument %s expected %s, got %s", name, mismatch.Expected, mismatch.Actual) + } + return fmt.Sprintf("argument %s type check failed: %s", name, err.Error()) +} + +func formatReturnTypeMismatch(fnName string, err error) string { + var mismatch *typeMismatchError + if errors.As(err, &mismatch) { + return fmt.Sprintf("return value for %s expected %s, got %s", fnName, mismatch.Expected, mismatch.Actual) + } + return fmt.Sprintf("return type check failed for %s: %s", fnName, err.Error()) +} + +type typeValidationVisit struct { + valueKind ValueKind + valueID uintptr + ty *TypeExpr +} + +type typeValidationState struct { + active map[typeValidationVisit]struct{} +} + +func valueMatchesType(val Value, ty *TypeExpr) (bool, error) { + state := typeValidationState{ + active: make(map[typeValidationVisit]struct{}), + } + return state.matches(val, ty) +} + +func (s *typeValidationState) matches(val Value, ty *TypeExpr) (bool, error) { + if visit, ok := typeValidationVisitFor(val, ty); ok { + if _, seen := s.active[visit]; seen { + // Recursive value/type pair already being validated higher in the stack. + return true, nil + } + s.active[visit] = struct{}{} + defer delete(s.active, visit) + } + + if ty.Nullable && val.Kind() == KindNil { + return true, nil + } + switch ty.Kind { + case TypeAny: + return true, nil + case TypeInt: + return val.Kind() == KindInt, nil + case TypeFloat: + return val.Kind() == KindFloat, nil + case TypeNumber: + return val.Kind() == KindInt || val.Kind() == KindFloat, nil + case TypeString: + return val.Kind() == KindString, nil + case TypeBool: + return val.Kind() == KindBool, nil + case TypeNil: + return val.Kind() == KindNil, nil + case TypeDuration: + return val.Kind() == KindDuration, nil + case TypeTime: + return val.Kind() == KindTime, nil + case TypeMoney: + return val.Kind() == KindMoney, nil + case TypeArray: + if val.Kind() != KindArray { + return false, nil + } + if len(ty.TypeArgs) == 0 { + return true, nil + } + if len(ty.TypeArgs) != 1 { + return false, fmt.Errorf("array type expects exactly 1 type argument") + } + elemType := ty.TypeArgs[0] + for _, elem := range val.Array() { + matches, err := s.matches(elem, elemType) + if err != nil { + return false, err + } + if !matches { + return false, nil + } + } + return true, nil + case TypeHash: + if val.Kind() != KindHash && val.Kind() != KindObject { + return false, nil + } + if len(ty.TypeArgs) == 0 { + return true, nil + } + if len(ty.TypeArgs) != 2 { + return false, fmt.Errorf("hash type expects exactly 2 type arguments") + } + keyType := ty.TypeArgs[0] + valueType := ty.TypeArgs[1] + for key, value := range val.Hash() { + keyMatches, err := s.matches(NewString(key), keyType) + if err != nil { + return false, err + } + if !keyMatches { + return false, nil + } + valueMatches, err := s.matches(value, valueType) + if err != nil { + return false, err + } + if !valueMatches { + return false, nil + } + } + return true, nil + case TypeFunction: + return val.Kind() == KindFunction, nil + case TypeShape: + if val.Kind() != KindHash && val.Kind() != KindObject { + return false, nil + } + entries := val.Hash() + if len(ty.Shape) == 0 { + return len(entries) == 0, nil + } + for field, fieldType := range ty.Shape { + fieldVal, ok := entries[field] + if !ok { + return false, nil + } + matches, err := s.matches(fieldVal, fieldType) + if err != nil { + return false, err + } + if !matches { + return false, nil + } + } + for field := range entries { + if _, ok := ty.Shape[field]; !ok { + return false, nil + } + } + return true, nil + case TypeUnion: + for _, option := range ty.Union { + matches, err := s.matches(val, option) + if err != nil { + return false, err + } + if matches { + return true, nil + } + } + return false, nil + default: + return false, fmt.Errorf("unknown type %s", ty.Name) + } +} + +func typeValidationVisitFor(val Value, ty *TypeExpr) (typeValidationVisit, bool) { + if ty == nil { + return typeValidationVisit{}, false + } + + var valueID uintptr + switch val.Kind() { + case KindArray: + valueID = reflect.ValueOf(val.Array()).Pointer() + case KindHash, KindObject: + valueID = reflect.ValueOf(val.Hash()).Pointer() + default: + return typeValidationVisit{}, false + } + if valueID == 0 { + return typeValidationVisit{}, false + } + + return typeValidationVisit{ + valueKind: val.Kind(), + valueID: valueID, + ty: ty, + }, true +} + +func formatTypeExpr(ty *TypeExpr) string { + if ty == nil { + return "unknown" + } + + if ty.Kind == TypeUnion { + if len(ty.Union) == 0 { + return "unknown" + } + parts := make([]string, len(ty.Union)) + for i, option := range ty.Union { + parts[i] = formatTypeExpr(option) + } + return strings.Join(parts, " | ") + } + + var name string + switch ty.Kind { + case TypeAny: + name = "any" + case TypeInt: + name = "int" + case TypeFloat: + name = "float" + case TypeNumber: + name = "number" + case TypeString: + name = "string" + case TypeBool: + name = "bool" + case TypeNil: + name = "nil" + case TypeDuration: + name = "duration" + case TypeTime: + name = "time" + case TypeMoney: + name = "money" + case TypeArray: + name = "array" + case TypeHash: + name = "hash" + case TypeFunction: + name = "function" + case TypeShape: + name = formatShapeType(ty) + default: + name = ty.Name + } + if name == "" { + name = "unknown" + } + if len(ty.TypeArgs) > 0 { + args := make([]string, len(ty.TypeArgs)) + for i, typeArg := range ty.TypeArgs { + args[i] = formatTypeExpr(typeArg) + } + name = fmt.Sprintf("%s<%s>", name, strings.Join(args, ", ")) + } + if ty.Nullable && !strings.HasSuffix(name, "?") { + return name + "?" + } + return name +} + +func formatShapeType(ty *TypeExpr) string { + if ty == nil || len(ty.Shape) == 0 { + return "{}" + } + fields := make([]string, 0, len(ty.Shape)) + for field := range ty.Shape { + fields = append(fields, field) + } + sort.Strings(fields) + parts := make([]string, len(fields)) + for i, field := range fields { + parts[i] = fmt.Sprintf("%s: %s", field, formatTypeExpr(ty.Shape[field])) + } + return "{ " + strings.Join(parts, ", ") + " }" +} + +func formatValueTypeExpr(val Value) string { + state := valueTypeFormatState{ + seenArrays: make(map[uintptr]struct{}), + seenHashes: make(map[uintptr]struct{}), + } + return state.format(val) +} + +type valueTypeFormatState struct { + seenArrays map[uintptr]struct{} + seenHashes map[uintptr]struct{} +} + +func (s *valueTypeFormatState) format(val Value) string { + switch val.Kind() { + case KindNil: + return "nil" + case KindBool: + return "bool" + case KindInt: + return "int" + case KindFloat: + return "float" + case KindString: + return "string" + case KindMoney: + return "money" + case KindDuration: + return "duration" + case KindTime: + return "time" + case KindSymbol: + return "symbol" + case KindRange: + return "range" + case KindFunction: + return "function" + case KindBuiltin: + return "builtin" + case KindBlock: + return "block" + case KindClass: + return "class" + case KindInstance: + return "instance" + case KindArray: + return s.formatArray(val.Array()) + case KindHash, KindObject: + return s.formatHash(val.Hash()) + default: + return val.Kind().String() + } +} + +func (s *valueTypeFormatState) formatArray(values []Value) string { + if len(values) == 0 { + return "array" + } + + id := reflect.ValueOf(values).Pointer() + if id != 0 { + if _, seen := s.seenArrays[id]; seen { + return "array<...>" + } + s.seenArrays[id] = struct{}{} + defer delete(s.seenArrays, id) + } + + elementTypes := make(map[string]struct{}, len(values)) + for _, value := range values { + elementTypes[s.format(value)] = struct{}{} + } + return "array<" + joinSortedTypes(elementTypes) + ">" +} + +func (s *valueTypeFormatState) formatHash(values map[string]Value) string { + if len(values) == 0 { + return "{}" + } + + id := reflect.ValueOf(values).Pointer() + if id != 0 { + if _, seen := s.seenHashes[id]; seen { + return "{ ... }" + } + s.seenHashes[id] = struct{}{} + defer delete(s.seenHashes, id) + } + + if len(values) <= 6 { + fields := make([]string, 0, len(values)) + for field := range values { + fields = append(fields, field) + } + sort.Strings(fields) + parts := make([]string, len(fields)) + for i, field := range fields { + parts[i] = fmt.Sprintf("%s: %s", field, s.format(values[field])) + } + return "{ " + strings.Join(parts, ", ") + " }" + } + + valueTypes := make(map[string]struct{}, len(values)) + for _, value := range values { + valueTypes[s.format(value)] = struct{}{} + } + return "hash" +} + +func joinSortedTypes(typeSet map[string]struct{}) string { + if len(typeSet) == 0 { + return "empty" + } + parts := make([]string, 0, len(typeSet)) + for typeName := range typeSet { + parts = append(parts, typeName) + } + sort.Strings(parts) + return strings.Join(parts, " | ") +} diff --git a/vibes/execution_values.go b/vibes/execution_values.go new file mode 100644 index 0000000..9feaafa --- /dev/null +++ b/vibes/execution_values.go @@ -0,0 +1,410 @@ +package vibes + +import ( + "errors" + "fmt" + "math" + "slices" + "time" +) + +func valueToHashKey(val Value) (string, error) { + switch val.Kind() { + case KindSymbol: + return val.String(), nil + case KindString: + return val.String(), nil + default: + return "", fmt.Errorf("unsupported hash key type %v", val.Kind()) + } +} + +func valueToInt64(val Value) (int64, error) { + switch val.Kind() { + case KindInt: + return val.Int(), nil + case KindFloat: + return int64(val.Float()), nil + default: + return 0, fmt.Errorf("expected integer value") + } +} + +func valueToInt(val Value) (int, error) { + switch val.Kind() { + case KindInt: + return int(val.Int()), nil + case KindFloat: + return int(val.Float()), nil + default: + return 0, fmt.Errorf("expected integer index") + } +} + +func sortComparisonResult(val Value) (int, error) { + switch val.Kind() { + case KindInt: + switch { + case val.Int() < 0: + return -1, nil + case val.Int() > 0: + return 1, nil + default: + return 0, nil + } + case KindFloat: + switch { + case val.Float() < 0: + return -1, nil + case val.Float() > 0: + return 1, nil + default: + return 0, nil + } + default: + return 0, fmt.Errorf("comparator must be numeric") + } +} + +func arraySortCompareValues(left, right Value) (int, error) { + switch { + case left.Kind() == KindInt && right.Kind() == KindInt: + switch { + case left.Int() < right.Int(): + return -1, nil + case left.Int() > right.Int(): + return 1, nil + default: + return 0, nil + } + case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): + switch { + case left.Float() < right.Float(): + return -1, nil + case left.Float() > right.Float(): + return 1, nil + default: + return 0, nil + } + case left.Kind() == KindString && right.Kind() == KindString: + switch { + case left.String() < right.String(): + return -1, nil + case left.String() > right.String(): + return 1, nil + default: + return 0, nil + } + case left.Kind() == KindSymbol && right.Kind() == KindSymbol: + switch { + case left.String() < right.String(): + return -1, nil + case left.String() > right.String(): + return 1, nil + default: + return 0, nil + } + case left.Kind() == KindBool && right.Kind() == KindBool: + switch { + case !left.Bool() && right.Bool(): + return -1, nil + case left.Bool() && !right.Bool(): + return 1, nil + default: + return 0, nil + } + case left.Kind() == KindDuration && right.Kind() == KindDuration: + switch { + case left.Duration().Seconds() < right.Duration().Seconds(): + return -1, nil + case left.Duration().Seconds() > right.Duration().Seconds(): + return 1, nil + default: + return 0, nil + } + case left.Kind() == KindTime && right.Kind() == KindTime: + switch { + case left.Time().Before(right.Time()): + return -1, nil + case left.Time().After(right.Time()): + return 1, nil + default: + return 0, nil + } + case left.Kind() == KindMoney && right.Kind() == KindMoney: + if left.Money().Currency() != right.Money().Currency() { + return 0, fmt.Errorf("money values with different currencies") + } + switch { + case left.Money().Cents() < right.Money().Cents(): + return -1, nil + case left.Money().Cents() > right.Money().Cents(): + return 1, nil + default: + return 0, nil + } + case left.Kind() == KindNil && right.Kind() == KindNil: + return 0, nil + default: + return 0, fmt.Errorf("values are not comparable") + } +} + +// flattenValues recursively flattens nested arrays up to the specified depth. +// depth=-1 means flatten completely (no limit). +// depth=0 means don't flatten at all. +// depth=1 means flatten one level, etc. +func flattenValues(values []Value, depth int) []Value { + out := make([]Value, 0, len(values)) + for _, v := range values { + if v.Kind() == KindArray && depth != 0 { + nextDepth := depth + if nextDepth > 0 { + nextDepth-- + } + out = append(out, flattenValues(v.Array(), nextDepth)...) + } else { + out = append(out, v) + } + } + return out +} + +func floatToInt64Checked(v float64, method string) (int64, error) { + if math.IsNaN(v) || math.IsInf(v, 0) { + return 0, fmt.Errorf("%s result out of int64 range", method) + } + // float64(math.MaxInt64) rounds to 2^63, so use >= 2^63 as the true upper bound. + if v < float64(math.MinInt64) || v >= math.Exp2(63) { + return 0, fmt.Errorf("%s result out of int64 range", method) + } + return int64(v), nil +} + +func addValues(left, right Value) (Value, error) { + switch { + case left.Kind() == KindInt && right.Kind() == KindInt: + return NewInt(left.Int() + right.Int()), nil + case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): + return NewFloat(left.Float() + right.Float()), nil + case left.Kind() == KindTime && right.Kind() == KindDuration: + return NewTime(left.Time().Add(time.Duration(right.Duration().Seconds()) * time.Second)), nil + case right.Kind() == KindTime && left.Kind() == KindDuration: + return NewTime(right.Time().Add(time.Duration(left.Duration().Seconds()) * time.Second)), nil + case left.Kind() == KindDuration && right.Kind() == KindDuration: + return NewDuration(Duration{seconds: left.Duration().Seconds() + right.Duration().Seconds()}), nil + case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): + secs, err := valueToInt64(right) + if err != nil { + return NewNil(), fmt.Errorf("unsupported addition operands") + } + return NewDuration(Duration{seconds: left.Duration().Seconds() + secs}), nil + case right.Kind() == KindDuration && (left.Kind() == KindInt || left.Kind() == KindFloat): + secs, err := valueToInt64(left) + if err != nil { + return NewNil(), fmt.Errorf("unsupported addition operands") + } + return NewDuration(Duration{seconds: right.Duration().Seconds() + secs}), nil + case left.Kind() == KindArray && right.Kind() == KindArray: + lArr := left.Array() + rArr := right.Array() + out := make([]Value, len(lArr)+len(rArr)) + copy(out, lArr) + copy(out[len(lArr):], rArr) + return NewArray(out), nil + case left.Kind() == KindString || right.Kind() == KindString: + return NewString(left.String() + right.String()), nil + case left.Kind() == KindMoney && right.Kind() == KindMoney: + sum, err := left.Money().add(right.Money()) + if err != nil { + return NewNil(), err + } + return NewMoney(sum), nil + default: + return NewNil(), fmt.Errorf("unsupported addition operands") + } +} + +func subtractValues(left, right Value) (Value, error) { + switch { + case left.Kind() == KindInt && right.Kind() == KindInt: + return NewInt(left.Int() - right.Int()), nil + case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): + return NewFloat(left.Float() - right.Float()), nil + case left.Kind() == KindTime && right.Kind() == KindDuration: + return NewTime(left.Time().Add(-time.Duration(right.Duration().Seconds()) * time.Second)), nil + case left.Kind() == KindTime && right.Kind() == KindTime: + diff := left.Time().Sub(right.Time()) + return NewDuration(Duration{seconds: int64(diff / time.Second)}), nil + case left.Kind() == KindDuration && right.Kind() == KindDuration: + return NewDuration(Duration{seconds: left.Duration().Seconds() - right.Duration().Seconds()}), nil + case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): + secs, err := valueToInt64(right) + if err != nil { + return NewNil(), fmt.Errorf("unsupported subtraction operands") + } + return NewDuration(Duration{seconds: left.Duration().Seconds() - secs}), nil + case left.Kind() == KindArray && right.Kind() == KindArray: + lArr := left.Array() + rArr := right.Array() + out := make([]Value, 0, len(lArr)) + for _, item := range lArr { + found := slices.ContainsFunc(rArr, item.Equal) + if !found { + out = append(out, item) + } + } + return NewArray(out), nil + case left.Kind() == KindMoney && right.Kind() == KindMoney: + diff, err := left.Money().sub(right.Money()) + if err != nil { + return NewNil(), err + } + return NewMoney(diff), nil + default: + return NewNil(), fmt.Errorf("unsupported subtraction operands") + } +} + +func multiplyValues(left, right Value) (Value, error) { + switch { + case left.Kind() == KindInt && right.Kind() == KindInt: + return NewInt(left.Int() * right.Int()), nil + case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): + return NewFloat(left.Float() * right.Float()), nil + case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): + secs, err := valueToInt64(right) + if err != nil { + return NewNil(), fmt.Errorf("unsupported multiplication operands") + } + return NewDuration(Duration{seconds: left.Duration().Seconds() * secs}), nil + case right.Kind() == KindDuration && (left.Kind() == KindInt || left.Kind() == KindFloat): + secs, err := valueToInt64(left) + if err != nil { + return NewNil(), fmt.Errorf("unsupported multiplication operands") + } + return NewDuration(Duration{seconds: right.Duration().Seconds() * secs}), nil + case left.Kind() == KindMoney && right.Kind() == KindInt: + return NewMoney(left.Money().mulInt(right.Int())), nil + case left.Kind() == KindInt && right.Kind() == KindMoney: + return NewMoney(right.Money().mulInt(left.Int())), nil + default: + return NewNil(), fmt.Errorf("unsupported multiplication operands") + } +} + +func divideValues(left, right Value) (Value, error) { + switch { + case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): + if right.Float() == 0 { + return NewNil(), errors.New("division by zero") + } + return NewFloat(left.Float() / right.Float()), nil + case left.Kind() == KindDuration && right.Kind() == KindDuration: + if right.Duration().Seconds() == 0 { + return NewNil(), errors.New("division by zero") + } + return NewFloat(float64(left.Duration().Seconds()) / float64(right.Duration().Seconds())), nil + case left.Kind() == KindDuration && (right.Kind() == KindInt || right.Kind() == KindFloat): + secs, err := valueToInt64(right) + if err != nil { + return NewNil(), fmt.Errorf("unsupported division operands") + } + if secs == 0 { + return NewNil(), errors.New("division by zero") + } + return NewDuration(Duration{seconds: left.Duration().Seconds() / secs}), nil + case left.Kind() == KindMoney && right.Kind() == KindInt: + res, err := left.Money().divInt(right.Int()) + if err != nil { + return NewNil(), err + } + return NewMoney(res), nil + default: + return NewNil(), fmt.Errorf("unsupported division operands") + } +} + +func moduloValues(left, right Value) (Value, error) { + if left.Kind() == KindInt && right.Kind() == KindInt { + if right.Int() == 0 { + return NewNil(), errors.New("modulo by zero") + } + return NewInt(left.Int() % right.Int()), nil + } + if left.Kind() == KindDuration && right.Kind() == KindDuration { + if right.Duration().Seconds() == 0 { + return NewNil(), errors.New("modulo by zero") + } + return NewDuration(Duration{seconds: left.Duration().Seconds() % right.Duration().Seconds()}), nil + } + return NewNil(), fmt.Errorf("unsupported modulo operands") +} + +func compareValues(expr *BinaryExpr, left, right Value, cmp func(int) bool) (Value, error) { + switch { + case left.Kind() == KindInt && right.Kind() == KindInt: + diff := left.Int() - right.Int() + switch { + case diff < 0: + return NewBool(cmp(-1)), nil + case diff > 0: + return NewBool(cmp(1)), nil + default: + return NewBool(cmp(0)), nil + } + case (left.Kind() == KindInt || left.Kind() == KindFloat) && (right.Kind() == KindInt || right.Kind() == KindFloat): + lf, rf := left.Float(), right.Float() + switch { + case lf < rf: + return NewBool(cmp(-1)), nil + case lf > rf: + return NewBool(cmp(1)), nil + default: + return NewBool(cmp(0)), nil + } + case left.Kind() == KindString && right.Kind() == KindString: + switch { + case left.String() < right.String(): + return NewBool(cmp(-1)), nil + case left.String() > right.String(): + return NewBool(cmp(1)), nil + default: + return NewBool(cmp(0)), nil + } + case left.Kind() == KindMoney && right.Kind() == KindMoney: + if left.Money().Currency() != right.Money().Currency() { + return NewNil(), fmt.Errorf("money currency mismatch for comparison") + } + diff := left.Money().Cents() - right.Money().Cents() + switch { + case diff < 0: + return NewBool(cmp(-1)), nil + case diff > 0: + return NewBool(cmp(1)), nil + default: + return NewBool(cmp(0)), nil + } + case left.Kind() == KindDuration && right.Kind() == KindDuration: + diff := left.Duration().Seconds() - right.Duration().Seconds() + switch { + case diff < 0: + return NewBool(cmp(-1)), nil + case diff > 0: + return NewBool(cmp(1)), nil + default: + return NewBool(cmp(0)), nil + } + case left.Kind() == KindTime && right.Kind() == KindTime: + switch { + case left.Time().Before(right.Time()): + return NewBool(cmp(-1)), nil + case left.Time().After(right.Time()): + return NewBool(cmp(1)), nil + default: + return NewBool(cmp(0)), nil + } + default: + return NewNil(), fmt.Errorf("unsupported comparison operands") + } +} From 4d7645dc6cb379a8b75568c937586a7eea43dc01 Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 21:52:10 -0500 Subject: [PATCH 02/99] Split parser statements and type parsing into dedicated files --- vibes/parser.go | 802 ------------------------------------- vibes/parser_statements.go | 614 ++++++++++++++++++++++++++++ vibes/parser_types.go | 200 +++++++++ 3 files changed, 814 insertions(+), 802 deletions(-) create mode 100644 vibes/parser_statements.go create mode 100644 vibes/parser_types.go diff --git a/vibes/parser.go b/vibes/parser.go index a1a84f4..db7ebdd 100644 --- a/vibes/parser.go +++ b/vibes/parser.go @@ -116,614 +116,6 @@ func (p *parser) ParseProgram() (*Program, []error) { return program, p.errors } -func (p *parser) parseStatement() Statement { - switch p.curToken.Type { - case tokenDef: - return p.parseFunctionStatement() - case tokenClass: - return p.parseClassStatement() - case tokenExport: - return p.parseExportStatement() - case tokenPrivate: - return p.parsePrivateStatement() - case tokenReturn: - return p.parseReturnStatement() - case tokenRaise: - return p.parseRaiseStatement() - case tokenIf: - return p.parseIfStatement() - case tokenFor: - return p.parseForStatement() - case tokenWhile: - return p.parseWhileStatement() - case tokenUntil: - return p.parseUntilStatement() - case tokenBreak: - return p.parseBreakStatement() - case tokenNext: - return p.parseNextStatement() - case tokenBegin: - return p.parseBeginStatement() - case tokenIdent: - if p.curToken.Literal == "assert" { - return p.parseAssertStatement() - } - return p.parseExpressionOrAssignStatement() - default: - return p.parseExpressionOrAssignStatement() - } -} - -func (p *parser) parseFunctionStatement() Statement { - pos := p.curToken.Pos - p.nextToken() - - isClassMethod := false - var name string - if p.curToken.Type == tokenSelf && p.peekToken.Type == tokenDot { - isClassMethod = true - p.nextToken() // consume dot - if !p.expectPeek(tokenIdent) { - return nil - } - name = p.curToken.Literal - p.nextToken() - } else { - if p.curToken.Type != tokenIdent { - p.errorExpected(p.curToken, "function name") - return nil - } - name = p.curToken.Literal - p.nextToken() - } - - if p.curToken.Type == tokenAssign { - name += "=" - p.nextToken() - } - - params := []Param{} - var returnTy *TypeExpr - // Optional parens on the same line - if p.curToken.Type == tokenLParen && p.curToken.Pos.Line == pos.Line { - if p.peekToken.Type == tokenRParen { - p.nextToken() // consume ')' - p.nextToken() - } else { - p.nextToken() - params = p.parseParams() - if !p.expectPeek(tokenRParen) { - return nil - } - p.nextToken() - } - } - if p.curToken.Type == tokenArrow { - p.nextToken() - returnTy = p.parseTypeExpr() - if returnTy == nil { - return nil - } - p.nextToken() - } - body := []Statement{} - p.statementNesting++ - defer func() { - p.statementNesting-- - }() - for p.curToken.Type != tokenEnd && p.curToken.Type != tokenEOF { - stmt := p.parseStatement() - if stmt != nil { - body = append(body, stmt) - } - p.nextToken() - } - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - } - - private := false - if p.insideClass && p.privateNext { - private = true - p.privateNext = false - } - - return &FunctionStmt{Name: name, Params: params, ReturnTy: returnTy, Body: body, IsClassMethod: isClassMethod, Private: private, position: pos} -} - -func (p *parser) parseExportStatement() Statement { - pos := p.curToken.Pos - if p.insideClass || p.statementNesting > 0 { - p.addParseError(pos, "export is only supported for top-level functions") - return nil - } - if !p.expectPeek(tokenDef) { - return nil - } - fnStmt := p.parseFunctionStatement() - if fnStmt == nil { - return nil - } - fn, ok := fnStmt.(*FunctionStmt) - if !ok { - p.addParseError(pos, "export expects a function definition") - return nil - } - if fn.IsClassMethod { - p.addParseError(pos, "export cannot be used with class methods") - return nil - } - fn.Exported = true - return fn -} - -func (p *parser) parsePrivateStatement() Statement { - pos := p.curToken.Pos - if p.insideClass || p.statementNesting > 0 { - p.addParseError(pos, "private is only supported for top-level functions and class methods") - return nil - } - if !p.expectPeek(tokenDef) { - return nil - } - fnStmt := p.parseFunctionStatement() - if fnStmt == nil { - return nil - } - fn, ok := fnStmt.(*FunctionStmt) - if !ok { - p.addParseError(pos, "private expects a function definition") - return nil - } - if fn.IsClassMethod { - p.addParseError(pos, "private cannot be used with class methods") - return nil - } - fn.Private = true - return fn -} - -func (p *parser) parseParams() []Param { - params := []Param{} - for { - if p.curToken.Type != tokenIdent && p.curToken.Type != tokenIvar { - p.errorExpected(p.curToken, "parameter name") - return params - } - param := Param{Name: p.curToken.Literal} - if p.curToken.Type == tokenIvar { - param.IsIvar = true - param.Name = strings.TrimPrefix(param.Name, "@") - } - if p.peekToken.Type == tokenColon { - p.nextToken() - p.nextToken() - param.Type = p.parseTypeExpr() - if param.Type == nil { - return params - } - } - if p.peekToken.Type == tokenAssign { - p.nextToken() - p.nextToken() - param.DefaultVal = p.parseExpression(lowestPrec) - } - params = append(params, param) - if p.peekToken.Type != tokenComma { - break - } - p.nextToken() - p.nextToken() - } - return params -} - -func (p *parser) parseReturnStatement() Statement { - pos := p.curToken.Pos - p.nextToken() - value := p.parseExpression(lowestPrec) - return &ReturnStmt{Value: value, position: pos} -} - -func (p *parser) parseRaiseStatement() Statement { - pos := p.curToken.Pos - if p.peekToken.Type == tokenEOF || p.peekToken.Type == tokenEnd || p.peekToken.Type == tokenEnsure || p.peekToken.Type == tokenRescue || p.peekToken.Pos.Line != pos.Line { - return &RaiseStmt{position: pos} - } - p.nextToken() - value := p.parseExpression(lowestPrec) - if value == nil { - return nil - } - return &RaiseStmt{Value: value, position: pos} -} - -func (p *parser) parseClassStatement() Statement { - pos := p.curToken.Pos - if !p.expectPeek(tokenIdent) { - return nil - } - name := p.curToken.Literal - p.nextToken() - - stmt := &ClassStmt{ - Name: name, - position: pos, - } - - prevInside := p.insideClass - prevPrivate := p.privateNext - p.insideClass = true - p.privateNext = false - p.statementNesting++ - defer func() { - p.statementNesting-- - }() - - for p.curToken.Type != tokenEnd && p.curToken.Type != tokenEOF { - switch p.curToken.Type { - case tokenDef: - fnStmt := p.parseFunctionStatement() - if fnStmt == nil { - return nil - } - fn := fnStmt.(*FunctionStmt) - if fn.IsClassMethod { - stmt.ClassMethods = append(stmt.ClassMethods, fn) - } else { - stmt.Methods = append(stmt.Methods, fn) - } - case tokenPrivate: - if p.peekToken.Type == tokenDef { - p.privateNext = true - p.nextToken() - continue - } - p.privateNext = true - case tokenProperty, tokenGetter, tokenSetter: - decl := p.parsePropertyDecl(p.curToken.Type) - stmt.Properties = append(stmt.Properties, decl) - default: - s := p.parseStatement() - if s != nil { - stmt.Body = append(stmt.Body, s) - } - } - p.nextToken() - } - - p.insideClass = prevInside - p.privateNext = prevPrivate - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - } - - return stmt -} - -func (p *parser) parsePropertyDecl(kind TokenType) PropertyDecl { - pos := p.curToken.Pos - names := []string{} - p.nextToken() - if p.curToken.Type == tokenIdent { - names = append(names, p.curToken.Literal) - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - if p.curToken.Type != tokenIdent { - p.errorExpected(p.curToken, "property name") - break - } - names = append(names, p.curToken.Literal) - } - } - return PropertyDecl{Names: names, Kind: strings.ToLower(string(kind)), position: pos} -} - -func (p *parser) parseIfStatement() Statement { - pos := p.curToken.Pos - p.nextToken() - condition := p.parseExpression(lowestPrec) - - p.nextToken() - consequent := p.parseBlock(tokenEnd, tokenElse, tokenElsif) - - var elseifClauses []*IfStmt - for p.curToken.Type == tokenElsif { - p.nextToken() - cond := p.parseExpression(lowestPrec) - p.nextToken() - body := p.parseBlock(tokenEnd, tokenElse, tokenElsif) - clause := &IfStmt{Condition: cond, Consequent: body, position: cond.Pos()} - elseifClauses = append(elseifClauses, clause) - } - - var alternate []Statement - if p.curToken.Type == tokenElse { - p.nextToken() - alternate = p.parseBlock(tokenEnd) - } - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - } - - return &IfStmt{Condition: condition, Consequent: consequent, ElseIf: elseifClauses, Alternate: alternate, position: pos} -} - -func (p *parser) parseForStatement() Statement { - pos := p.curToken.Pos - if !p.expectPeek(tokenIdent) { - return nil - } - iterator := p.curToken.Literal - - if !p.expectPeek(tokenIn) { - return nil - } - - p.nextToken() - iterable := p.parseExpression(lowestPrec) - - p.nextToken() - body := p.parseBlock(tokenEnd) - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - } - - return &ForStmt{Iterator: iterator, Iterable: iterable, Body: body, position: pos} -} - -func (p *parser) parseWhileStatement() Statement { - pos := p.curToken.Pos - p.nextToken() - condition := p.parseExpression(lowestPrec) - - p.nextToken() - body := p.parseBlock(tokenEnd) - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - } - - return &WhileStmt{Condition: condition, Body: body, position: pos} -} - -func (p *parser) parseUntilStatement() Statement { - pos := p.curToken.Pos - p.nextToken() - condition := p.parseExpression(lowestPrec) - - p.nextToken() - body := p.parseBlock(tokenEnd) - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - } - - return &UntilStmt{Condition: condition, Body: body, position: pos} -} - -func (p *parser) parseBreakStatement() Statement { - return &BreakStmt{position: p.curToken.Pos} -} - -func (p *parser) parseNextStatement() Statement { - return &NextStmt{position: p.curToken.Pos} -} - -func (p *parser) parseBeginStatement() Statement { - pos := p.curToken.Pos - p.nextToken() - body := p.parseBlock(tokenRescue, tokenEnsure, tokenEnd) - - var rescueTy *TypeExpr - var rescueBody []Statement - if p.curToken.Type == tokenRescue { - rescuePos := p.curToken.Pos - if p.peekToken.Type == tokenLParen && p.peekToken.Pos.Line == rescuePos.Line { - p.nextToken() - p.nextToken() - rescueTy = p.parseTypeExpr() - if rescueTy == nil { - return nil - } - if !p.validateRescueTypeExpr(rescueTy, rescuePos) { - return nil - } - if !p.expectPeek(tokenRParen) { - return nil - } - } - p.nextToken() - rescueBody = p.parseBlock(tokenEnsure, tokenEnd) - } - - var ensureBody []Statement - if p.curToken.Type == tokenEnsure { - p.nextToken() - ensureBody = p.parseBlock(tokenEnd) - } - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - return nil - } - - if len(rescueBody) == 0 && len(ensureBody) == 0 { - p.addParseError(pos, "begin requires rescue and/or ensure") - return nil - } - - return &TryStmt{Body: body, RescueTy: rescueTy, Rescue: rescueBody, Ensure: ensureBody, position: pos} -} - -func (p *parser) validateRescueTypeExpr(ty *TypeExpr, pos Position) bool { - if ty == nil { - p.addParseError(pos, "rescue type cannot be empty") - return false - } - - if ty.Kind == TypeUnion { - ok := true - for _, option := range ty.Union { - if !p.validateRescueTypeExpr(option, option.position) { - ok = false - } - } - return ok - } - - if len(ty.TypeArgs) > 0 || len(ty.Shape) > 0 { - p.addParseError(pos, fmt.Sprintf("rescue type must be an error class, got %s", formatTypeExpr(ty))) - return false - } - if _, ok := canonicalRuntimeErrorType(ty.Name); !ok { - p.addParseError(pos, fmt.Sprintf("unknown rescue error type %s", ty.Name)) - return false - } - return true -} - -func (p *parser) parseBlock(stop ...TokenType) []Statement { - stmts := []Statement{} - stopSet := make(map[TokenType]struct{}, len(stop)) - for _, tt := range stop { - stopSet[tt] = struct{}{} - } - p.statementNesting++ - defer func() { - p.statementNesting-- - }() - - for { - if _, ok := stopSet[p.curToken.Type]; ok || p.curToken.Type == tokenEOF { - return stmts - } - stmt := p.parseStatement() - if stmt != nil { - stmts = append(stmts, stmt) - } - p.nextToken() - } -} - -func (p *parser) parseExpressionOrAssignStatement() Statement { - expr := p.parseExpression(lowestPrec) - if expr == nil { - return nil - } - - if p.peekToken.Type == tokenDo { - p.nextToken() - block := p.parseBlockLiteral() - var call *CallExpr - if existing, ok := expr.(*CallExpr); ok { - call = existing - } else { - call = &CallExpr{Callee: expr, position: expr.Pos()} - } - call.Block = block - expr = call - } - - if p.peekToken.Type == tokenAssign && isAssignable(expr) { - pos := expr.Pos() - p.nextToken() - p.nextToken() - value := p.parseExpressionWithBlock() - return &AssignStmt{Target: expr, Value: value, position: pos} - } - - return &ExprStmt{Expr: expr, position: expr.Pos()} -} - -func (p *parser) parseExpressionWithBlock() Expression { - expr := p.parseExpression(lowestPrec) - if expr == nil { - return nil - } - if p.peekToken.Type == tokenDo { - p.nextToken() - block := p.parseBlockLiteral() - var call *CallExpr - if existing, ok := expr.(*CallExpr); ok { - call = existing - } else { - call = &CallExpr{Callee: expr, position: expr.Pos()} - } - call.Block = block - return call - } - return expr -} -func (p *parser) parseAssertStatement() Statement { - pos := p.curToken.Pos - callee := &Identifier{Name: p.curToken.Literal, position: pos} - args := []Expression{} - p.nextToken() - if p.curToken.Type == tokenEOF || p.curToken.Type == tokenEnd { - return &ExprStmt{Expr: callee, position: pos} - } - first := p.parseExpression(lowestPrec) - if first != nil { - args = append(args, first) - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - args = append(args, p.parseExpression(lowestPrec)) - } - } - call := &CallExpr{Callee: callee, Args: args, position: pos} - return &ExprStmt{Expr: call, position: pos} -} - -func isAssignable(expr Expression) bool { - switch expr.(type) { - case *Identifier, *MemberExpr, *IndexExpr, *IvarExpr, *ClassVarExpr: - return true - default: - return false - } -} - -const ( - lowestPrec = iota - precAssign - precOr - precAnd - precEquality - precComparison - precRange - precSum - precProduct - precPrefix - precCall -) - -var precedences = map[TokenType]int{ - tokenOr: precOr, - tokenAnd: precAnd, - tokenEQ: precEquality, - tokenNotEQ: precEquality, - tokenLT: precComparison, - tokenLTE: precComparison, - tokenGT: precComparison, - tokenGTE: precComparison, - tokenRange: precRange, - tokenPlus: precSum, - tokenMinus: precSum, - tokenSlash: precProduct, - tokenAsterisk: precProduct, - tokenPercent: precProduct, - tokenLParen: precCall, - tokenDot: precCall, - tokenLBracket: precCall, -} - func (p *parser) parseExpression(precedence int) Expression { prefix := p.prefixFns[p.curToken.Type] if prefix == nil { @@ -1317,197 +709,3 @@ func tokenLabel(tt TokenType) string { return fmt.Sprintf("%q", strings.ToLower(string(tt))) } } - -func resolveType(name string) (TypeKind, bool) { - nullable := false - if strings.HasSuffix(name, "?") { - nullable = true - name = strings.TrimSuffix(name, "?") - } - switch strings.ToLower(name) { - case "any": - return TypeAny, nullable - case "int": - return TypeInt, nullable - case "float": - return TypeFloat, nullable - case "number": - return TypeNumber, nullable - case "string": - return TypeString, nullable - case "bool": - return TypeBool, nullable - case "nil": - return TypeNil, nullable - case "duration": - return TypeDuration, nullable - case "time": - return TypeTime, nullable - case "money": - return TypeMoney, nullable - case "array": - return TypeArray, nullable - case "hash", "object": - return TypeHash, nullable - case "function": - return TypeFunction, nullable - } - return TypeUnknown, nullable -} - -func (p *parser) parseTypeExpr() *TypeExpr { - first := p.parseTypeAtom() - if first == nil { - return nil - } - - union := []*TypeExpr{first} - for p.peekToken.Type == tokenPipe { - p.nextToken() - p.nextToken() - next := p.parseTypeAtom() - if next == nil { - return nil - } - union = append(union, next) - } - - if len(union) == 1 { - return first - } - - names := make([]string, len(union)) - for i, option := range union { - names[i] = formatTypeExpr(option) - } - return &TypeExpr{ - Name: strings.Join(names, " | "), - Kind: TypeUnion, - Union: union, - position: first.position, - } -} - -func (p *parser) parseTypeAtom() *TypeExpr { - if p.curToken.Type == tokenLBrace { - return p.parseTypeShape() - } - if p.curToken.Type != tokenIdent && p.curToken.Type != tokenNil { - p.errorExpected(p.curToken, "type name") - return nil - } - ty := &TypeExpr{Name: p.curToken.Literal, position: p.curToken.Pos} - kind, nullable := resolveType(p.curToken.Literal) - ty.Kind = kind - ty.Nullable = nullable - - if p.peekToken.Type == tokenLT { - if ty.Kind != TypeArray && ty.Kind != TypeHash { - p.addParseError(p.curToken.Pos, fmt.Sprintf("type %s does not accept type arguments", ty.Name)) - return nil - } - p.nextToken() - p.nextToken() - typeArgs := []*TypeExpr{} - for { - arg := p.parseTypeExpr() - if arg == nil { - return nil - } - typeArgs = append(typeArgs, arg) - - if p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - continue - } - - if p.peekToken.Type != tokenGT { - p.errorExpected(p.peekToken, ">") - return nil - } - p.nextToken() - break - } - ty.TypeArgs = typeArgs - switch ty.Kind { - case TypeArray: - if len(typeArgs) != 1 { - p.addParseError(ty.position, "array type expects exactly 1 type argument") - return nil - } - case TypeHash: - if len(typeArgs) != 2 { - p.addParseError(ty.position, "hash type expects exactly 2 type arguments") - return nil - } - } - } - - return ty -} - -func (p *parser) parseTypeShape() *TypeExpr { - pos := p.curToken.Pos - fields := make(map[string]*TypeExpr) - - if p.peekToken.Type == tokenRBrace { - p.nextToken() - return &TypeExpr{ - Kind: TypeShape, - Shape: fields, - position: pos, - } - } - - p.nextToken() - for { - key, ok := p.parseTypeShapeFieldName() - if !ok { - return nil - } - if p.peekToken.Type != tokenColon { - p.errorExpected(p.peekToken, ":") - return nil - } - p.nextToken() - p.nextToken() - fieldType := p.parseTypeExpr() - if fieldType == nil { - return nil - } - if _, exists := fields[key]; exists { - p.addParseError(p.curToken.Pos, fmt.Sprintf("duplicate shape field %s", key)) - return nil - } - fields[key] = fieldType - - if p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - continue - } - if p.peekToken.Type != tokenRBrace { - p.errorExpected(p.peekToken, "}") - return nil - } - p.nextToken() - break - } - - return &TypeExpr{ - Kind: TypeShape, - Shape: fields, - position: pos, - } -} - -func (p *parser) parseTypeShapeFieldName() (string, bool) { - switch p.curToken.Type { - case tokenIdent, tokenString, tokenSymbol: - return p.curToken.Literal, true - default: - p.errorExpected(p.curToken, "shape field name") - return "", false - } -} diff --git a/vibes/parser_statements.go b/vibes/parser_statements.go new file mode 100644 index 0000000..dee897b --- /dev/null +++ b/vibes/parser_statements.go @@ -0,0 +1,614 @@ +package vibes + +import ( + "fmt" + "strings" +) + +func (p *parser) parseStatement() Statement { + switch p.curToken.Type { + case tokenDef: + return p.parseFunctionStatement() + case tokenClass: + return p.parseClassStatement() + case tokenExport: + return p.parseExportStatement() + case tokenPrivate: + return p.parsePrivateStatement() + case tokenReturn: + return p.parseReturnStatement() + case tokenRaise: + return p.parseRaiseStatement() + case tokenIf: + return p.parseIfStatement() + case tokenFor: + return p.parseForStatement() + case tokenWhile: + return p.parseWhileStatement() + case tokenUntil: + return p.parseUntilStatement() + case tokenBreak: + return p.parseBreakStatement() + case tokenNext: + return p.parseNextStatement() + case tokenBegin: + return p.parseBeginStatement() + case tokenIdent: + if p.curToken.Literal == "assert" { + return p.parseAssertStatement() + } + return p.parseExpressionOrAssignStatement() + default: + return p.parseExpressionOrAssignStatement() + } +} + +func (p *parser) parseFunctionStatement() Statement { + pos := p.curToken.Pos + p.nextToken() + + isClassMethod := false + var name string + if p.curToken.Type == tokenSelf && p.peekToken.Type == tokenDot { + isClassMethod = true + p.nextToken() // consume dot + if !p.expectPeek(tokenIdent) { + return nil + } + name = p.curToken.Literal + p.nextToken() + } else { + if p.curToken.Type != tokenIdent { + p.errorExpected(p.curToken, "function name") + return nil + } + name = p.curToken.Literal + p.nextToken() + } + + if p.curToken.Type == tokenAssign { + name += "=" + p.nextToken() + } + + params := []Param{} + var returnTy *TypeExpr + // Optional parens on the same line + if p.curToken.Type == tokenLParen && p.curToken.Pos.Line == pos.Line { + if p.peekToken.Type == tokenRParen { + p.nextToken() // consume ')' + p.nextToken() + } else { + p.nextToken() + params = p.parseParams() + if !p.expectPeek(tokenRParen) { + return nil + } + p.nextToken() + } + } + if p.curToken.Type == tokenArrow { + p.nextToken() + returnTy = p.parseTypeExpr() + if returnTy == nil { + return nil + } + p.nextToken() + } + body := []Statement{} + p.statementNesting++ + defer func() { + p.statementNesting-- + }() + for p.curToken.Type != tokenEnd && p.curToken.Type != tokenEOF { + stmt := p.parseStatement() + if stmt != nil { + body = append(body, stmt) + } + p.nextToken() + } + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + } + + private := false + if p.insideClass && p.privateNext { + private = true + p.privateNext = false + } + + return &FunctionStmt{Name: name, Params: params, ReturnTy: returnTy, Body: body, IsClassMethod: isClassMethod, Private: private, position: pos} +} + +func (p *parser) parseExportStatement() Statement { + pos := p.curToken.Pos + if p.insideClass || p.statementNesting > 0 { + p.addParseError(pos, "export is only supported for top-level functions") + return nil + } + if !p.expectPeek(tokenDef) { + return nil + } + fnStmt := p.parseFunctionStatement() + if fnStmt == nil { + return nil + } + fn, ok := fnStmt.(*FunctionStmt) + if !ok { + p.addParseError(pos, "export expects a function definition") + return nil + } + if fn.IsClassMethod { + p.addParseError(pos, "export cannot be used with class methods") + return nil + } + fn.Exported = true + return fn +} + +func (p *parser) parsePrivateStatement() Statement { + pos := p.curToken.Pos + if p.insideClass || p.statementNesting > 0 { + p.addParseError(pos, "private is only supported for top-level functions and class methods") + return nil + } + if !p.expectPeek(tokenDef) { + return nil + } + fnStmt := p.parseFunctionStatement() + if fnStmt == nil { + return nil + } + fn, ok := fnStmt.(*FunctionStmt) + if !ok { + p.addParseError(pos, "private expects a function definition") + return nil + } + if fn.IsClassMethod { + p.addParseError(pos, "private cannot be used with class methods") + return nil + } + fn.Private = true + return fn +} + +func (p *parser) parseParams() []Param { + params := []Param{} + for { + if p.curToken.Type != tokenIdent && p.curToken.Type != tokenIvar { + p.errorExpected(p.curToken, "parameter name") + return params + } + param := Param{Name: p.curToken.Literal} + if p.curToken.Type == tokenIvar { + param.IsIvar = true + param.Name = strings.TrimPrefix(param.Name, "@") + } + if p.peekToken.Type == tokenColon { + p.nextToken() + p.nextToken() + param.Type = p.parseTypeExpr() + if param.Type == nil { + return params + } + } + if p.peekToken.Type == tokenAssign { + p.nextToken() + p.nextToken() + param.DefaultVal = p.parseExpression(lowestPrec) + } + params = append(params, param) + if p.peekToken.Type != tokenComma { + break + } + p.nextToken() + p.nextToken() + } + return params +} + +func (p *parser) parseReturnStatement() Statement { + pos := p.curToken.Pos + p.nextToken() + value := p.parseExpression(lowestPrec) + return &ReturnStmt{Value: value, position: pos} +} + +func (p *parser) parseRaiseStatement() Statement { + pos := p.curToken.Pos + if p.peekToken.Type == tokenEOF || p.peekToken.Type == tokenEnd || p.peekToken.Type == tokenEnsure || p.peekToken.Type == tokenRescue || p.peekToken.Pos.Line != pos.Line { + return &RaiseStmt{position: pos} + } + p.nextToken() + value := p.parseExpression(lowestPrec) + if value == nil { + return nil + } + return &RaiseStmt{Value: value, position: pos} +} + +func (p *parser) parseClassStatement() Statement { + pos := p.curToken.Pos + if !p.expectPeek(tokenIdent) { + return nil + } + name := p.curToken.Literal + p.nextToken() + + stmt := &ClassStmt{ + Name: name, + position: pos, + } + + prevInside := p.insideClass + prevPrivate := p.privateNext + p.insideClass = true + p.privateNext = false + p.statementNesting++ + defer func() { + p.statementNesting-- + }() + + for p.curToken.Type != tokenEnd && p.curToken.Type != tokenEOF { + switch p.curToken.Type { + case tokenDef: + fnStmt := p.parseFunctionStatement() + if fnStmt == nil { + return nil + } + fn := fnStmt.(*FunctionStmt) + if fn.IsClassMethod { + stmt.ClassMethods = append(stmt.ClassMethods, fn) + } else { + stmt.Methods = append(stmt.Methods, fn) + } + case tokenPrivate: + if p.peekToken.Type == tokenDef { + p.privateNext = true + p.nextToken() + continue + } + p.privateNext = true + case tokenProperty, tokenGetter, tokenSetter: + decl := p.parsePropertyDecl(p.curToken.Type) + stmt.Properties = append(stmt.Properties, decl) + default: + s := p.parseStatement() + if s != nil { + stmt.Body = append(stmt.Body, s) + } + } + p.nextToken() + } + + p.insideClass = prevInside + p.privateNext = prevPrivate + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + } + + return stmt +} + +func (p *parser) parsePropertyDecl(kind TokenType) PropertyDecl { + pos := p.curToken.Pos + names := []string{} + p.nextToken() + if p.curToken.Type == tokenIdent { + names = append(names, p.curToken.Literal) + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + if p.curToken.Type != tokenIdent { + p.errorExpected(p.curToken, "property name") + break + } + names = append(names, p.curToken.Literal) + } + } + return PropertyDecl{Names: names, Kind: strings.ToLower(string(kind)), position: pos} +} + +func (p *parser) parseIfStatement() Statement { + pos := p.curToken.Pos + p.nextToken() + condition := p.parseExpression(lowestPrec) + + p.nextToken() + consequent := p.parseBlock(tokenEnd, tokenElse, tokenElsif) + + var elseifClauses []*IfStmt + for p.curToken.Type == tokenElsif { + p.nextToken() + cond := p.parseExpression(lowestPrec) + p.nextToken() + body := p.parseBlock(tokenEnd, tokenElse, tokenElsif) + clause := &IfStmt{Condition: cond, Consequent: body, position: cond.Pos()} + elseifClauses = append(elseifClauses, clause) + } + + var alternate []Statement + if p.curToken.Type == tokenElse { + p.nextToken() + alternate = p.parseBlock(tokenEnd) + } + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + } + + return &IfStmt{Condition: condition, Consequent: consequent, ElseIf: elseifClauses, Alternate: alternate, position: pos} +} + +func (p *parser) parseForStatement() Statement { + pos := p.curToken.Pos + if !p.expectPeek(tokenIdent) { + return nil + } + iterator := p.curToken.Literal + + if !p.expectPeek(tokenIn) { + return nil + } + + p.nextToken() + iterable := p.parseExpression(lowestPrec) + + p.nextToken() + body := p.parseBlock(tokenEnd) + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + } + + return &ForStmt{Iterator: iterator, Iterable: iterable, Body: body, position: pos} +} + +func (p *parser) parseWhileStatement() Statement { + pos := p.curToken.Pos + p.nextToken() + condition := p.parseExpression(lowestPrec) + + p.nextToken() + body := p.parseBlock(tokenEnd) + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + } + + return &WhileStmt{Condition: condition, Body: body, position: pos} +} + +func (p *parser) parseUntilStatement() Statement { + pos := p.curToken.Pos + p.nextToken() + condition := p.parseExpression(lowestPrec) + + p.nextToken() + body := p.parseBlock(tokenEnd) + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + } + + return &UntilStmt{Condition: condition, Body: body, position: pos} +} + +func (p *parser) parseBreakStatement() Statement { + return &BreakStmt{position: p.curToken.Pos} +} + +func (p *parser) parseNextStatement() Statement { + return &NextStmt{position: p.curToken.Pos} +} + +func (p *parser) parseBeginStatement() Statement { + pos := p.curToken.Pos + p.nextToken() + body := p.parseBlock(tokenRescue, tokenEnsure, tokenEnd) + + var rescueTy *TypeExpr + var rescueBody []Statement + if p.curToken.Type == tokenRescue { + rescuePos := p.curToken.Pos + if p.peekToken.Type == tokenLParen && p.peekToken.Pos.Line == rescuePos.Line { + p.nextToken() + p.nextToken() + rescueTy = p.parseTypeExpr() + if rescueTy == nil { + return nil + } + if !p.validateRescueTypeExpr(rescueTy, rescuePos) { + return nil + } + if !p.expectPeek(tokenRParen) { + return nil + } + } + p.nextToken() + rescueBody = p.parseBlock(tokenEnsure, tokenEnd) + } + + var ensureBody []Statement + if p.curToken.Type == tokenEnsure { + p.nextToken() + ensureBody = p.parseBlock(tokenEnd) + } + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + return nil + } + + if len(rescueBody) == 0 && len(ensureBody) == 0 { + p.addParseError(pos, "begin requires rescue and/or ensure") + return nil + } + + return &TryStmt{Body: body, RescueTy: rescueTy, Rescue: rescueBody, Ensure: ensureBody, position: pos} +} + +func (p *parser) validateRescueTypeExpr(ty *TypeExpr, pos Position) bool { + if ty == nil { + p.addParseError(pos, "rescue type cannot be empty") + return false + } + + if ty.Kind == TypeUnion { + ok := true + for _, option := range ty.Union { + if !p.validateRescueTypeExpr(option, option.position) { + ok = false + } + } + return ok + } + + if len(ty.TypeArgs) > 0 || len(ty.Shape) > 0 { + p.addParseError(pos, fmt.Sprintf("rescue type must be an error class, got %s", formatTypeExpr(ty))) + return false + } + if _, ok := canonicalRuntimeErrorType(ty.Name); !ok { + p.addParseError(pos, fmt.Sprintf("unknown rescue error type %s", ty.Name)) + return false + } + return true +} + +func (p *parser) parseBlock(stop ...TokenType) []Statement { + stmts := []Statement{} + stopSet := make(map[TokenType]struct{}, len(stop)) + for _, tt := range stop { + stopSet[tt] = struct{}{} + } + p.statementNesting++ + defer func() { + p.statementNesting-- + }() + + for { + if _, ok := stopSet[p.curToken.Type]; ok || p.curToken.Type == tokenEOF { + return stmts + } + stmt := p.parseStatement() + if stmt != nil { + stmts = append(stmts, stmt) + } + p.nextToken() + } +} + +func (p *parser) parseExpressionOrAssignStatement() Statement { + expr := p.parseExpression(lowestPrec) + if expr == nil { + return nil + } + + if p.peekToken.Type == tokenDo { + p.nextToken() + block := p.parseBlockLiteral() + var call *CallExpr + if existing, ok := expr.(*CallExpr); ok { + call = existing + } else { + call = &CallExpr{Callee: expr, position: expr.Pos()} + } + call.Block = block + expr = call + } + + if p.peekToken.Type == tokenAssign && isAssignable(expr) { + pos := expr.Pos() + p.nextToken() + p.nextToken() + value := p.parseExpressionWithBlock() + return &AssignStmt{Target: expr, Value: value, position: pos} + } + + return &ExprStmt{Expr: expr, position: expr.Pos()} +} + +func (p *parser) parseExpressionWithBlock() Expression { + expr := p.parseExpression(lowestPrec) + if expr == nil { + return nil + } + if p.peekToken.Type == tokenDo { + p.nextToken() + block := p.parseBlockLiteral() + var call *CallExpr + if existing, ok := expr.(*CallExpr); ok { + call = existing + } else { + call = &CallExpr{Callee: expr, position: expr.Pos()} + } + call.Block = block + return call + } + return expr +} +func (p *parser) parseAssertStatement() Statement { + pos := p.curToken.Pos + callee := &Identifier{Name: p.curToken.Literal, position: pos} + args := []Expression{} + p.nextToken() + if p.curToken.Type == tokenEOF || p.curToken.Type == tokenEnd { + return &ExprStmt{Expr: callee, position: pos} + } + first := p.parseExpression(lowestPrec) + if first != nil { + args = append(args, first) + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + args = append(args, p.parseExpression(lowestPrec)) + } + } + call := &CallExpr{Callee: callee, Args: args, position: pos} + return &ExprStmt{Expr: call, position: pos} +} + +func isAssignable(expr Expression) bool { + switch expr.(type) { + case *Identifier, *MemberExpr, *IndexExpr, *IvarExpr, *ClassVarExpr: + return true + default: + return false + } +} + +const ( + lowestPrec = iota + precAssign + precOr + precAnd + precEquality + precComparison + precRange + precSum + precProduct + precPrefix + precCall +) + +var precedences = map[TokenType]int{ + tokenOr: precOr, + tokenAnd: precAnd, + tokenEQ: precEquality, + tokenNotEQ: precEquality, + tokenLT: precComparison, + tokenLTE: precComparison, + tokenGT: precComparison, + tokenGTE: precComparison, + tokenRange: precRange, + tokenPlus: precSum, + tokenMinus: precSum, + tokenSlash: precProduct, + tokenAsterisk: precProduct, + tokenPercent: precProduct, + tokenLParen: precCall, + tokenDot: precCall, + tokenLBracket: precCall, +} diff --git a/vibes/parser_types.go b/vibes/parser_types.go new file mode 100644 index 0000000..70bb8c5 --- /dev/null +++ b/vibes/parser_types.go @@ -0,0 +1,200 @@ +package vibes + +import ( + "fmt" + "strings" +) + +func resolveType(name string) (TypeKind, bool) { + nullable := false + if strings.HasSuffix(name, "?") { + nullable = true + name = strings.TrimSuffix(name, "?") + } + switch strings.ToLower(name) { + case "any": + return TypeAny, nullable + case "int": + return TypeInt, nullable + case "float": + return TypeFloat, nullable + case "number": + return TypeNumber, nullable + case "string": + return TypeString, nullable + case "bool": + return TypeBool, nullable + case "nil": + return TypeNil, nullable + case "duration": + return TypeDuration, nullable + case "time": + return TypeTime, nullable + case "money": + return TypeMoney, nullable + case "array": + return TypeArray, nullable + case "hash", "object": + return TypeHash, nullable + case "function": + return TypeFunction, nullable + } + return TypeUnknown, nullable +} + +func (p *parser) parseTypeExpr() *TypeExpr { + first := p.parseTypeAtom() + if first == nil { + return nil + } + + union := []*TypeExpr{first} + for p.peekToken.Type == tokenPipe { + p.nextToken() + p.nextToken() + next := p.parseTypeAtom() + if next == nil { + return nil + } + union = append(union, next) + } + + if len(union) == 1 { + return first + } + + names := make([]string, len(union)) + for i, option := range union { + names[i] = formatTypeExpr(option) + } + return &TypeExpr{ + Name: strings.Join(names, " | "), + Kind: TypeUnion, + Union: union, + position: first.position, + } +} + +func (p *parser) parseTypeAtom() *TypeExpr { + if p.curToken.Type == tokenLBrace { + return p.parseTypeShape() + } + if p.curToken.Type != tokenIdent && p.curToken.Type != tokenNil { + p.errorExpected(p.curToken, "type name") + return nil + } + ty := &TypeExpr{Name: p.curToken.Literal, position: p.curToken.Pos} + kind, nullable := resolveType(p.curToken.Literal) + ty.Kind = kind + ty.Nullable = nullable + + if p.peekToken.Type == tokenLT { + if ty.Kind != TypeArray && ty.Kind != TypeHash { + p.addParseError(p.curToken.Pos, fmt.Sprintf("type %s does not accept type arguments", ty.Name)) + return nil + } + p.nextToken() + p.nextToken() + typeArgs := []*TypeExpr{} + for { + arg := p.parseTypeExpr() + if arg == nil { + return nil + } + typeArgs = append(typeArgs, arg) + + if p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + continue + } + + if p.peekToken.Type != tokenGT { + p.errorExpected(p.peekToken, ">") + return nil + } + p.nextToken() + break + } + ty.TypeArgs = typeArgs + switch ty.Kind { + case TypeArray: + if len(typeArgs) != 1 { + p.addParseError(ty.position, "array type expects exactly 1 type argument") + return nil + } + case TypeHash: + if len(typeArgs) != 2 { + p.addParseError(ty.position, "hash type expects exactly 2 type arguments") + return nil + } + } + } + + return ty +} + +func (p *parser) parseTypeShape() *TypeExpr { + pos := p.curToken.Pos + fields := make(map[string]*TypeExpr) + + if p.peekToken.Type == tokenRBrace { + p.nextToken() + return &TypeExpr{ + Kind: TypeShape, + Shape: fields, + position: pos, + } + } + + p.nextToken() + for { + key, ok := p.parseTypeShapeFieldName() + if !ok { + return nil + } + if p.peekToken.Type != tokenColon { + p.errorExpected(p.peekToken, ":") + return nil + } + p.nextToken() + p.nextToken() + fieldType := p.parseTypeExpr() + if fieldType == nil { + return nil + } + if _, exists := fields[key]; exists { + p.addParseError(p.curToken.Pos, fmt.Sprintf("duplicate shape field %s", key)) + return nil + } + fields[key] = fieldType + + if p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + continue + } + if p.peekToken.Type != tokenRBrace { + p.errorExpected(p.peekToken, "}") + return nil + } + p.nextToken() + break + } + + return &TypeExpr{ + Kind: TypeShape, + Shape: fields, + position: pos, + } +} + +func (p *parser) parseTypeShapeFieldName() (string, bool) { + switch p.curToken.Type { + case tokenIdent, tokenString, tokenSymbol: + return p.curToken.Literal, true + default: + p.errorExpected(p.curToken, "shape field name") + return "", false + } +} From 046da9ebd64eeed4f94fb41affda3c2e703f4f3e Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 21:53:53 -0500 Subject: [PATCH 03/99] Split builtins by domain and centralize builtin registration --- vibes/builtins.go | 445 ----------------------------------- vibes/builtins_json_regex.go | 375 +++++++++++++++++++++++++++++ vibes/builtins_numeric.go | 81 +++++++ vibes/interpreter.go | 34 ++- 4 files changed, 481 insertions(+), 454 deletions(-) create mode 100644 vibes/builtins_json_regex.go create mode 100644 vibes/builtins_numeric.go diff --git a/vibes/builtins.go b/vibes/builtins.go index 429c9de..4e39158 100644 --- a/vibes/builtins.go +++ b/vibes/builtins.go @@ -2,16 +2,8 @@ package vibes import ( "encoding/hex" - "encoding/json" "fmt" - "io" - "math" - "reflect" - "regexp" - "strconv" - "strings" "time" - "unicode/utf8" ) const ( @@ -168,79 +160,6 @@ func builtinRandomID(exec *Execution, receiver Value, args []Value, kwargs map[s return NewString(string(chars)), nil } -func builtinToInt(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("to_int expects a single value argument") - } - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("to_int does not accept keyword arguments") - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("to_int does not accept blocks") - } - - switch args[0].Kind() { - case KindInt: - return args[0], nil - case KindFloat: - f := args[0].Float() - if math.Trunc(f) != f { - return NewNil(), fmt.Errorf("to_int cannot convert non-integer float") - } - n, err := floatToInt64Checked(f, "to_int") - if err != nil { - return NewNil(), err - } - return NewInt(n), nil - case KindString: - s := strings.TrimSpace(args[0].String()) - if s == "" { - return NewNil(), fmt.Errorf("to_int expects a numeric string") - } - n, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return NewNil(), fmt.Errorf("to_int expects a base-10 integer string") - } - return NewInt(n), nil - default: - return NewNil(), fmt.Errorf("to_int expects int, float, or string") - } -} - -func builtinToFloat(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("to_float expects a single value argument") - } - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("to_float does not accept keyword arguments") - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("to_float does not accept blocks") - } - - switch args[0].Kind() { - case KindInt: - return NewFloat(float64(args[0].Int())), nil - case KindFloat: - return args[0], nil - case KindString: - s := strings.TrimSpace(args[0].String()) - if s == "" { - return NewNil(), fmt.Errorf("to_float expects a numeric string") - } - f, err := strconv.ParseFloat(s, 64) - if err != nil { - return NewNil(), fmt.Errorf("to_float expects a numeric string") - } - if math.IsNaN(f) || math.IsInf(f, 0) { - return NewNil(), fmt.Errorf("to_float expects a finite numeric string") - } - return NewFloat(f), nil - default: - return NewNil(), fmt.Errorf("to_float expects int, float, or string") - } -} - func formatUUID(raw []byte) string { hexValue := hex.EncodeToString(raw) return hexValue[0:8] + "-" + hexValue[8:12] + "-" + hexValue[12:16] + "-" + hexValue[16:20] + "-" + hexValue[20:32] @@ -250,367 +169,3 @@ type jsonStringifyState struct { seenArrays map[uintptr]struct{} seenHashes map[uintptr]struct{} } - -func builtinJSONParse(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 || args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("JSON.parse expects a single JSON string argument") - } - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("JSON.parse does not accept keyword arguments") - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("JSON.parse does not accept blocks") - } - - raw := args[0].String() - if len(raw) > maxJSONPayloadBytes { - return NewNil(), fmt.Errorf("JSON.parse input exceeds limit %d bytes", maxJSONPayloadBytes) - } - - decoder := json.NewDecoder(strings.NewReader(raw)) - decoder.UseNumber() - - var decoded any - if err := decoder.Decode(&decoded); err != nil { - return NewNil(), fmt.Errorf("JSON.parse invalid JSON: %v", err) - } - if err := decoder.Decode(&struct{}{}); err != io.EOF { - return NewNil(), fmt.Errorf("JSON.parse invalid JSON: trailing data") - } - - value, err := jsonValueToVibeValue(decoded) - if err != nil { - return NewNil(), err - } - return value, nil -} - -func builtinJSONStringify(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("JSON.stringify expects a single value argument") - } - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("JSON.stringify does not accept keyword arguments") - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("JSON.stringify does not accept blocks") - } - - state := &jsonStringifyState{ - seenArrays: map[uintptr]struct{}{}, - seenHashes: map[uintptr]struct{}{}, - } - encoded, err := vibeValueToJSONValue(args[0], state) - if err != nil { - return NewNil(), err - } - - payload, err := json.Marshal(encoded) - if err != nil { - return NewNil(), fmt.Errorf("JSON.stringify failed: %v", err) - } - if len(payload) > maxJSONPayloadBytes { - return NewNil(), fmt.Errorf("JSON.stringify output exceeds limit %d bytes", maxJSONPayloadBytes) - } - return NewString(string(payload)), nil -} - -func jsonValueToVibeValue(val any) (Value, error) { - switch v := val.(type) { - case nil: - return NewNil(), nil - case bool: - return NewBool(v), nil - case string: - return NewString(v), nil - case json.Number: - if i, err := v.Int64(); err == nil { - return NewInt(i), nil - } - f, err := v.Float64() - if err != nil { - return NewNil(), fmt.Errorf("JSON.parse invalid number %q", v.String()) - } - return NewFloat(f), nil - case float64: - return NewFloat(v), nil - case []any: - arr := make([]Value, len(v)) - for i, item := range v { - converted, err := jsonValueToVibeValue(item) - if err != nil { - return NewNil(), err - } - arr[i] = converted - } - return NewArray(arr), nil - case map[string]any: - obj := make(map[string]Value, len(v)) - for key, item := range v { - converted, err := jsonValueToVibeValue(item) - if err != nil { - return NewNil(), err - } - obj[key] = converted - } - return NewHash(obj), nil - default: - return NewNil(), fmt.Errorf("JSON.parse unsupported value type %T", val) - } -} - -func vibeValueToJSONValue(val Value, state *jsonStringifyState) (any, error) { - switch val.Kind() { - case KindNil: - return nil, nil - case KindBool: - return val.Bool(), nil - case KindInt: - return val.Int(), nil - case KindFloat: - return val.Float(), nil - case KindString, KindSymbol: - return val.String(), nil - case KindArray: - arr := val.Array() - id := reflect.ValueOf(arr).Pointer() - if id != 0 { - if _, seen := state.seenArrays[id]; seen { - return nil, fmt.Errorf("JSON.stringify does not support cyclic arrays") - } - state.seenArrays[id] = struct{}{} - defer delete(state.seenArrays, id) - } - - out := make([]any, len(arr)) - for i, item := range arr { - converted, err := vibeValueToJSONValue(item, state) - if err != nil { - return nil, err - } - out[i] = converted - } - return out, nil - case KindHash, KindObject: - hash := val.Hash() - id := reflect.ValueOf(hash).Pointer() - if id != 0 { - if _, seen := state.seenHashes[id]; seen { - return nil, fmt.Errorf("JSON.stringify does not support cyclic objects") - } - state.seenHashes[id] = struct{}{} - defer delete(state.seenHashes, id) - } - - out := make(map[string]any, len(hash)) - for key, item := range hash { - converted, err := vibeValueToJSONValue(item, state) - if err != nil { - return nil, err - } - out[key] = converted - } - return out, nil - default: - return nil, fmt.Errorf("JSON.stringify unsupported value type %s", val.Kind()) - } -} - -func builtinRegexMatch(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("Regex.match expects pattern and text") - } - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("Regex.match does not accept keyword arguments") - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("Regex.match does not accept blocks") - } - if args[0].Kind() != KindString || args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("Regex.match expects string pattern and text") - } - pattern := args[0].String() - text := args[1].String() - if len(pattern) > maxRegexPatternSize { - return NewNil(), fmt.Errorf("Regex.match pattern exceeds limit %d bytes", maxRegexPatternSize) - } - if len(text) > maxRegexInputBytes { - return NewNil(), fmt.Errorf("Regex.match text exceeds limit %d bytes", maxRegexInputBytes) - } - - re, err := regexp.Compile(pattern) - if err != nil { - return NewNil(), fmt.Errorf("Regex.match invalid regex: %v", err) - } - indices := re.FindStringIndex(text) - if indices == nil { - return NewNil(), nil - } - return NewString(text[indices[0]:indices[1]]), nil -} - -func builtinRegexReplace(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - return builtinRegexReplaceInternal(args, kwargs, block, false) -} - -func builtinRegexReplaceAll(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - return builtinRegexReplaceInternal(args, kwargs, block, true) -} - -func builtinRegexReplaceInternal(args []Value, kwargs map[string]Value, block Value, replaceAll bool) (Value, error) { - method := "Regex.replace" - if replaceAll { - method = "Regex.replace_all" - } - - if len(args) != 3 { - return NewNil(), fmt.Errorf("%s expects text, pattern, replacement", method) - } - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("%s does not accept keyword arguments", method) - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("%s does not accept blocks", method) - } - if args[0].Kind() != KindString || args[1].Kind() != KindString || args[2].Kind() != KindString { - return NewNil(), fmt.Errorf("%s expects string text, pattern, replacement", method) - } - - text := args[0].String() - pattern := args[1].String() - replacement := args[2].String() - if len(pattern) > maxRegexPatternSize { - return NewNil(), fmt.Errorf("%s pattern exceeds limit %d bytes", method, maxRegexPatternSize) - } - if len(text) > maxRegexInputBytes { - return NewNil(), fmt.Errorf("%s text exceeds limit %d bytes", method, maxRegexInputBytes) - } - if len(replacement) > maxRegexInputBytes { - return NewNil(), fmt.Errorf("%s replacement exceeds limit %d bytes", method, maxRegexInputBytes) - } - - re, err := regexp.Compile(pattern) - if err != nil { - return NewNil(), fmt.Errorf("%s invalid regex: %v", method, err) - } - - if replaceAll { - replaced, err := regexReplaceAllWithLimit(re, text, replacement, method) - if err != nil { - return NewNil(), err - } - return NewString(replaced), nil - } - - loc := re.FindStringSubmatchIndex(text) - if loc == nil { - return NewString(text), nil - } - replaced := string(re.ExpandString(nil, replacement, text, loc)) - outputLen := len(text) - (loc[1] - loc[0]) + len(replaced) - if outputLen > maxRegexInputBytes { - return NewNil(), fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) - } - return NewString(text[:loc[0]] + replaced + text[loc[1]:]), nil -} - -func regexReplaceAllWithLimit(re *regexp.Regexp, text string, replacement string, method string) (string, error) { - out := make([]byte, 0, len(text)) - lastAppended := 0 - searchStart := 0 - lastMatchEnd := -1 - for searchStart <= len(text) { - loc, found := nextRegexReplaceAllSubmatchIndex(re, text, searchStart) - if !found { - break - } - if loc[0] == loc[1] && loc[0] == lastMatchEnd { - if loc[0] >= len(text) { - break - } - _, size := utf8.DecodeRuneInString(text[loc[0]:]) - if size == 0 { - size = 1 - } - searchStart = loc[0] + size - continue - } - - segmentLen := loc[0] - lastAppended - if len(out) > maxRegexInputBytes-segmentLen { - return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) - } - out = append(out, text[lastAppended:loc[0]]...) - out = re.ExpandString(out, replacement, text, loc) - if len(out) > maxRegexInputBytes { - return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) - } - lastAppended = loc[1] - lastMatchEnd = loc[1] - - if loc[1] > loc[0] { - searchStart = loc[1] - continue - } - if loc[1] >= len(text) { - break - } - _, size := utf8.DecodeRuneInString(text[loc[1]:]) - if size == 0 { - size = 1 - } - searchStart = loc[1] + size - } - - tailLen := len(text) - lastAppended - if len(out) > maxRegexInputBytes-tailLen { - return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) - } - out = append(out, text[lastAppended:]...) - return string(out), nil -} - -func nextRegexReplaceAllSubmatchIndex(re *regexp.Regexp, text string, start int) ([]int, bool) { - loc := re.FindStringSubmatchIndex(text[start:]) - if loc == nil { - return nil, false - } - direct := offsetRegexSubmatchIndex(loc, start) - if start == 0 || direct[0] > start { - return direct, true - } - - windowStart := start - 1 - locs := re.FindAllStringSubmatchIndex(text[windowStart:], 2) - if len(locs) == 0 { - return nil, false - } - - first := offsetRegexSubmatchIndex(locs[0], windowStart) - if first[0] >= start { - return first, true - } - if first[1] > start { - return direct, true - } - if len(locs) < 2 { - return nil, false - } - second := offsetRegexSubmatchIndex(locs[1], windowStart) - if second[0] >= start { - return second, true - } - return nil, false -} - -func offsetRegexSubmatchIndex(loc []int, offset int) []int { - abs := make([]int, len(loc)) - for i, index := range loc { - if index < 0 { - abs[i] = -1 - continue - } - abs[i] = index + offset - } - return abs -} diff --git a/vibes/builtins_json_regex.go b/vibes/builtins_json_regex.go new file mode 100644 index 0000000..956da94 --- /dev/null +++ b/vibes/builtins_json_regex.go @@ -0,0 +1,375 @@ +package vibes + +import ( + "encoding/json" + "fmt" + "io" + "reflect" + "regexp" + "strings" + "unicode/utf8" +) + +func builtinJSONParse(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 || args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("JSON.parse expects a single JSON string argument") + } + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("JSON.parse does not accept keyword arguments") + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("JSON.parse does not accept blocks") + } + + raw := args[0].String() + if len(raw) > maxJSONPayloadBytes { + return NewNil(), fmt.Errorf("JSON.parse input exceeds limit %d bytes", maxJSONPayloadBytes) + } + + decoder := json.NewDecoder(strings.NewReader(raw)) + decoder.UseNumber() + + var decoded any + if err := decoder.Decode(&decoded); err != nil { + return NewNil(), fmt.Errorf("JSON.parse invalid JSON: %v", err) + } + if err := decoder.Decode(&struct{}{}); err != io.EOF { + return NewNil(), fmt.Errorf("JSON.parse invalid JSON: trailing data") + } + + value, err := jsonValueToVibeValue(decoded) + if err != nil { + return NewNil(), err + } + return value, nil +} + +func builtinJSONStringify(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("JSON.stringify expects a single value argument") + } + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("JSON.stringify does not accept keyword arguments") + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("JSON.stringify does not accept blocks") + } + + state := &jsonStringifyState{ + seenArrays: map[uintptr]struct{}{}, + seenHashes: map[uintptr]struct{}{}, + } + encoded, err := vibeValueToJSONValue(args[0], state) + if err != nil { + return NewNil(), err + } + + payload, err := json.Marshal(encoded) + if err != nil { + return NewNil(), fmt.Errorf("JSON.stringify failed: %v", err) + } + if len(payload) > maxJSONPayloadBytes { + return NewNil(), fmt.Errorf("JSON.stringify output exceeds limit %d bytes", maxJSONPayloadBytes) + } + return NewString(string(payload)), nil +} + +func jsonValueToVibeValue(val any) (Value, error) { + switch v := val.(type) { + case nil: + return NewNil(), nil + case bool: + return NewBool(v), nil + case string: + return NewString(v), nil + case json.Number: + if i, err := v.Int64(); err == nil { + return NewInt(i), nil + } + f, err := v.Float64() + if err != nil { + return NewNil(), fmt.Errorf("JSON.parse invalid number %q", v.String()) + } + return NewFloat(f), nil + case float64: + return NewFloat(v), nil + case []any: + arr := make([]Value, len(v)) + for i, item := range v { + converted, err := jsonValueToVibeValue(item) + if err != nil { + return NewNil(), err + } + arr[i] = converted + } + return NewArray(arr), nil + case map[string]any: + obj := make(map[string]Value, len(v)) + for key, item := range v { + converted, err := jsonValueToVibeValue(item) + if err != nil { + return NewNil(), err + } + obj[key] = converted + } + return NewHash(obj), nil + default: + return NewNil(), fmt.Errorf("JSON.parse unsupported value type %T", val) + } +} + +func vibeValueToJSONValue(val Value, state *jsonStringifyState) (any, error) { + switch val.Kind() { + case KindNil: + return nil, nil + case KindBool: + return val.Bool(), nil + case KindInt: + return val.Int(), nil + case KindFloat: + return val.Float(), nil + case KindString, KindSymbol: + return val.String(), nil + case KindArray: + arr := val.Array() + id := reflect.ValueOf(arr).Pointer() + if id != 0 { + if _, seen := state.seenArrays[id]; seen { + return nil, fmt.Errorf("JSON.stringify does not support cyclic arrays") + } + state.seenArrays[id] = struct{}{} + defer delete(state.seenArrays, id) + } + + out := make([]any, len(arr)) + for i, item := range arr { + converted, err := vibeValueToJSONValue(item, state) + if err != nil { + return nil, err + } + out[i] = converted + } + return out, nil + case KindHash, KindObject: + hash := val.Hash() + id := reflect.ValueOf(hash).Pointer() + if id != 0 { + if _, seen := state.seenHashes[id]; seen { + return nil, fmt.Errorf("JSON.stringify does not support cyclic objects") + } + state.seenHashes[id] = struct{}{} + defer delete(state.seenHashes, id) + } + + out := make(map[string]any, len(hash)) + for key, item := range hash { + converted, err := vibeValueToJSONValue(item, state) + if err != nil { + return nil, err + } + out[key] = converted + } + return out, nil + default: + return nil, fmt.Errorf("JSON.stringify unsupported value type %s", val.Kind()) + } +} + +func builtinRegexMatch(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("Regex.match expects pattern and text") + } + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("Regex.match does not accept keyword arguments") + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("Regex.match does not accept blocks") + } + if args[0].Kind() != KindString || args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("Regex.match expects string pattern and text") + } + pattern := args[0].String() + text := args[1].String() + if len(pattern) > maxRegexPatternSize { + return NewNil(), fmt.Errorf("Regex.match pattern exceeds limit %d bytes", maxRegexPatternSize) + } + if len(text) > maxRegexInputBytes { + return NewNil(), fmt.Errorf("Regex.match text exceeds limit %d bytes", maxRegexInputBytes) + } + + re, err := regexp.Compile(pattern) + if err != nil { + return NewNil(), fmt.Errorf("Regex.match invalid regex: %v", err) + } + indices := re.FindStringIndex(text) + if indices == nil { + return NewNil(), nil + } + return NewString(text[indices[0]:indices[1]]), nil +} + +func builtinRegexReplace(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + return builtinRegexReplaceInternal(args, kwargs, block, false) +} + +func builtinRegexReplaceAll(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + return builtinRegexReplaceInternal(args, kwargs, block, true) +} + +func builtinRegexReplaceInternal(args []Value, kwargs map[string]Value, block Value, replaceAll bool) (Value, error) { + method := "Regex.replace" + if replaceAll { + method = "Regex.replace_all" + } + + if len(args) != 3 { + return NewNil(), fmt.Errorf("%s expects text, pattern, replacement", method) + } + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("%s does not accept keyword arguments", method) + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("%s does not accept blocks", method) + } + if args[0].Kind() != KindString || args[1].Kind() != KindString || args[2].Kind() != KindString { + return NewNil(), fmt.Errorf("%s expects string text, pattern, replacement", method) + } + + text := args[0].String() + pattern := args[1].String() + replacement := args[2].String() + if len(pattern) > maxRegexPatternSize { + return NewNil(), fmt.Errorf("%s pattern exceeds limit %d bytes", method, maxRegexPatternSize) + } + if len(text) > maxRegexInputBytes { + return NewNil(), fmt.Errorf("%s text exceeds limit %d bytes", method, maxRegexInputBytes) + } + if len(replacement) > maxRegexInputBytes { + return NewNil(), fmt.Errorf("%s replacement exceeds limit %d bytes", method, maxRegexInputBytes) + } + + re, err := regexp.Compile(pattern) + if err != nil { + return NewNil(), fmt.Errorf("%s invalid regex: %v", method, err) + } + + if replaceAll { + replaced, err := regexReplaceAllWithLimit(re, text, replacement, method) + if err != nil { + return NewNil(), err + } + return NewString(replaced), nil + } + + loc := re.FindStringSubmatchIndex(text) + if loc == nil { + return NewString(text), nil + } + replaced := string(re.ExpandString(nil, replacement, text, loc)) + outputLen := len(text) - (loc[1] - loc[0]) + len(replaced) + if outputLen > maxRegexInputBytes { + return NewNil(), fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) + } + return NewString(text[:loc[0]] + replaced + text[loc[1]:]), nil +} + +func regexReplaceAllWithLimit(re *regexp.Regexp, text string, replacement string, method string) (string, error) { + out := make([]byte, 0, len(text)) + lastAppended := 0 + searchStart := 0 + lastMatchEnd := -1 + for searchStart <= len(text) { + loc, found := nextRegexReplaceAllSubmatchIndex(re, text, searchStart) + if !found { + break + } + if loc[0] == loc[1] && loc[0] == lastMatchEnd { + if loc[0] >= len(text) { + break + } + _, size := utf8.DecodeRuneInString(text[loc[0]:]) + if size == 0 { + size = 1 + } + searchStart = loc[0] + size + continue + } + + segmentLen := loc[0] - lastAppended + if len(out) > maxRegexInputBytes-segmentLen { + return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) + } + out = append(out, text[lastAppended:loc[0]]...) + out = re.ExpandString(out, replacement, text, loc) + if len(out) > maxRegexInputBytes { + return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) + } + lastAppended = loc[1] + lastMatchEnd = loc[1] + + if loc[1] > loc[0] { + searchStart = loc[1] + continue + } + if loc[1] >= len(text) { + break + } + _, size := utf8.DecodeRuneInString(text[loc[1]:]) + if size == 0 { + size = 1 + } + searchStart = loc[1] + size + } + + tailLen := len(text) - lastAppended + if len(out) > maxRegexInputBytes-tailLen { + return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes) + } + out = append(out, text[lastAppended:]...) + return string(out), nil +} + +func nextRegexReplaceAllSubmatchIndex(re *regexp.Regexp, text string, start int) ([]int, bool) { + loc := re.FindStringSubmatchIndex(text[start:]) + if loc == nil { + return nil, false + } + direct := offsetRegexSubmatchIndex(loc, start) + if start == 0 || direct[0] > start { + return direct, true + } + + windowStart := start - 1 + locs := re.FindAllStringSubmatchIndex(text[windowStart:], 2) + if len(locs) == 0 { + return nil, false + } + + first := offsetRegexSubmatchIndex(locs[0], windowStart) + if first[0] >= start { + return first, true + } + if first[1] > start { + return direct, true + } + if len(locs) < 2 { + return nil, false + } + second := offsetRegexSubmatchIndex(locs[1], windowStart) + if second[0] >= start { + return second, true + } + return nil, false +} + +func offsetRegexSubmatchIndex(loc []int, offset int) []int { + abs := make([]int, len(loc)) + for i, index := range loc { + if index < 0 { + abs[i] = -1 + continue + } + abs[i] = index + offset + } + return abs +} diff --git a/vibes/builtins_numeric.go b/vibes/builtins_numeric.go new file mode 100644 index 0000000..2cc2f86 --- /dev/null +++ b/vibes/builtins_numeric.go @@ -0,0 +1,81 @@ +package vibes + +import ( + "fmt" + "math" + "strconv" + "strings" +) + +func builtinToInt(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("to_int expects a single value argument") + } + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("to_int does not accept keyword arguments") + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("to_int does not accept blocks") + } + + switch args[0].Kind() { + case KindInt: + return args[0], nil + case KindFloat: + f := args[0].Float() + if math.Trunc(f) != f { + return NewNil(), fmt.Errorf("to_int cannot convert non-integer float") + } + n, err := floatToInt64Checked(f, "to_int") + if err != nil { + return NewNil(), err + } + return NewInt(n), nil + case KindString: + s := strings.TrimSpace(args[0].String()) + if s == "" { + return NewNil(), fmt.Errorf("to_int expects a numeric string") + } + n, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return NewNil(), fmt.Errorf("to_int expects a base-10 integer string") + } + return NewInt(n), nil + default: + return NewNil(), fmt.Errorf("to_int expects int, float, or string") + } +} + +func builtinToFloat(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("to_float expects a single value argument") + } + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("to_float does not accept keyword arguments") + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("to_float does not accept blocks") + } + + switch args[0].Kind() { + case KindInt: + return NewFloat(float64(args[0].Int())), nil + case KindFloat: + return args[0], nil + case KindString: + s := strings.TrimSpace(args[0].String()) + if s == "" { + return NewNil(), fmt.Errorf("to_float expects a numeric string") + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return NewNil(), fmt.Errorf("to_float expects a numeric string") + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return NewNil(), fmt.Errorf("to_float expects a finite numeric string") + } + return NewFloat(f), nil + default: + return NewNil(), fmt.Errorf("to_float expects int, float, or string") + } +} diff --git a/vibes/interpreter.go b/vibes/interpreter.go index 9e30097..b8089b4 100644 --- a/vibes/interpreter.go +++ b/vibes/interpreter.go @@ -70,15 +70,7 @@ func NewEngine(cfg Config) (*Engine, error) { modPaths: append([]string(nil), cfg.ModulePaths...), } - engine.RegisterBuiltin("assert", builtinAssert) - engine.RegisterBuiltin("money", builtinMoney) - engine.RegisterBuiltin("money_cents", builtinMoneyCents) - engine.RegisterBuiltin("require", builtinRequire) - engine.RegisterZeroArgBuiltin("now", builtinNow) - engine.RegisterZeroArgBuiltin("uuid", builtinUUID) - engine.RegisterBuiltin("random_id", builtinRandomID) - engine.RegisterBuiltin("to_int", builtinToInt) - engine.RegisterBuiltin("to_float", builtinToFloat) + registerCoreBuiltins(engine) engine.builtins["JSON"] = NewObject(map[string]Value{ "parse": NewBuiltin("JSON.parse", builtinJSONParse), "stringify": NewBuiltin("JSON.stringify", builtinJSONStringify), @@ -324,6 +316,30 @@ func (e *Engine) RegisterZeroArgBuiltin(name string, fn BuiltinFunc) { e.builtins[name] = NewAutoBuiltin(name, fn) } +func registerCoreBuiltins(engine *Engine) { + for _, builtin := range []struct { + name string + fn BuiltinFunc + autoInvoke bool + }{ + {name: "assert", fn: builtinAssert}, + {name: "money", fn: builtinMoney}, + {name: "money_cents", fn: builtinMoneyCents}, + {name: "require", fn: builtinRequire}, + {name: "now", fn: builtinNow, autoInvoke: true}, + {name: "uuid", fn: builtinUUID, autoInvoke: true}, + {name: "random_id", fn: builtinRandomID}, + {name: "to_int", fn: builtinToInt}, + {name: "to_float", fn: builtinToFloat}, + } { + if builtin.autoInvoke { + engine.RegisterZeroArgBuiltin(builtin.name, builtin.fn) + continue + } + engine.RegisterBuiltin(builtin.name, builtin.fn) + } +} + // Builtins returns a copy of the registered builtin map. func (e *Engine) Builtins() map[string]Value { out := make(map[string]Value, len(e.builtins)) From 3c9453d2811bf7f482680580c47bfe89382189dc Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 21:55:05 -0500 Subject: [PATCH 04/99] Extract require/export flow into modules_require file --- vibes/modules.go | 228 -------------------------------------- vibes/modules_require.go | 234 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 228 deletions(-) create mode 100644 vibes/modules_require.go diff --git a/vibes/modules.go b/vibes/modules.go index fb471ec..b8fee75 100644 --- a/vibes/modules.go +++ b/vibes/modules.go @@ -7,7 +7,6 @@ import ( "os" "path" "path/filepath" - "reflect" "slices" "strings" ) @@ -390,230 +389,3 @@ func cloneFunctionForEnv(fn *ScriptFunction, env *Env) *ScriptFunction { clone.Env = env return &clone } - -func moduleCycleFromLoadStack(stack []string, next string) ([]string, bool) { - for idx, key := range stack { - if key == next { - cycle := append(append([]string(nil), stack[idx:]...), next) - return cycle, true - } - } - return nil, false -} - -func moduleExecutionChain(stack []moduleContext) []string { - chain := make([]string, 0, len(stack)) - for _, ctx := range stack { - if ctx.key == "" { - continue - } - if len(chain) > 0 && chain[len(chain)-1] == ctx.key { - continue - } - chain = append(chain, ctx.key) - } - return chain -} - -func moduleCycleFromExecution(stack []moduleContext, next string) ([]string, bool) { - chain := moduleExecutionChain(stack) - if len(chain) < 2 { - return nil, false - } - for idx, key := range chain[:len(chain)-1] { - if key == next { - cycle := append(append([]string(nil), chain[idx:]...), next) - return cycle, true - } - } - return nil, false -} - -func formatModuleCycle(cycle []string) string { - if len(cycle) == 0 { - return "" - } - normalized := make([]string, 0, len(cycle)) - for _, key := range cycle { - if len(normalized) > 0 && normalized[len(normalized)-1] == key { - continue - } - normalized = append(normalized, key) - } - parts := make([]string, len(normalized)) - for idx, key := range normalized { - parts[idx] = moduleDisplayName(key) - } - return strings.Join(parts, " -> ") -} - -func shouldExportModuleFunction(fn *ScriptFunction) bool { - return fn != nil && !fn.Private -} - -func parseRequireAlias(kwargs map[string]Value) (string, error) { - if len(kwargs) == 0 { - return "", nil - } - if len(kwargs) != 1 { - for key := range kwargs { - if key != "as" { - return "", fmt.Errorf("require: unknown keyword argument %s", key) - } - } - return "", fmt.Errorf("require: unknown keyword arguments") - } - - aliasVal, ok := kwargs["as"] - if !ok { - for key := range kwargs { - return "", fmt.Errorf("require: unknown keyword argument %s", key) - } - return "", fmt.Errorf("require: unknown keyword arguments") - } - - var aliasName string - switch aliasVal.Kind() { - case KindString, KindSymbol: - aliasName = strings.TrimSpace(aliasVal.String()) - default: - return "", fmt.Errorf("require: alias must be a string or symbol") - } - - if !isValidModuleAlias(aliasName) { - return "", fmt.Errorf("require: invalid alias %q", aliasName) - } - - return aliasName, nil -} - -func isValidModuleAlias(name string) bool { - if name == "" { - return false - } - runes := []rune(name) - if len(runes) == 0 || !isIdentifierStart(runes[0]) { - return false - } - for _, r := range runes[1:] { - if !isIdentifierRune(r) { - return false - } - } - return lookupIdent(name) == tokenIdent -} - -func bindRequireAlias(root *Env, alias string, module Value) error { - if err := validateRequireAliasBinding(root, alias, module); err != nil { - return err - } - if alias == "" { - return nil - } - root.Define(alias, module) - return nil -} - -func validateRequireAliasBinding(root *Env, alias string, module Value) error { - if alias == "" { - return nil - } - if existing, ok := root.Get(alias); ok { - if existing.Kind() == KindObject && module.Kind() == KindObject && reflect.ValueOf(existing.Hash()).Pointer() == reflect.ValueOf(module.Hash()).Pointer() { - return nil - } - return fmt.Errorf("require: alias %q already defined", alias) - } - return nil -} - -func bindModuleExportsWithoutOverwrite(root *Env, exports map[string]Value) { - for name, fnVal := range exports { - if _, exists := root.Get(name); exists { - continue - } - root.Define(name, fnVal) - } -} - -func builtinRequire(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if exec.strictEffects && !exec.allowRequire { - return NewNil(), fmt.Errorf("strict effects: require is disabled without CallOptions.AllowRequire") - } - if len(args) != 1 { - return NewNil(), fmt.Errorf("require expects a single module name argument") - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("require does not accept blocks") - } - if exec.root == nil { - return NewNil(), fmt.Errorf("require unavailable in this context") - } - alias, err := parseRequireAlias(kwargs) - if err != nil { - return NewNil(), err - } - - modNameVal := args[0] - switch modNameVal.Kind() { - case KindString, KindSymbol: - // supported - default: - return NewNil(), fmt.Errorf("require expects a string or symbol module name") - } - - entry, err := exec.engine.loadModule(modNameVal.String(), exec.currentModuleContext()) - if err != nil { - return NewNil(), err - } - - if cycle, ok := moduleCycleFromLoadStack(exec.moduleLoadStack, entry.key); ok { - return NewNil(), fmt.Errorf("require: circular dependency detected: %s", formatModuleCycle(cycle)) - } - - if cached, ok := exec.modules[entry.key]; ok { - if err := bindRequireAlias(exec.root, alias, cached); err != nil { - return NewNil(), err - } - return cached, nil - } - - if cycle, ok := moduleCycleFromExecution(exec.moduleStack, entry.key); ok { - return NewNil(), fmt.Errorf("require: circular dependency detected: %s", formatModuleCycle(cycle)) - } - - if exec.moduleLoading[entry.key] { - cycle := append(append([]string(nil), exec.moduleLoadStack...), entry.key) - return NewNil(), fmt.Errorf("require: circular dependency detected: %s", formatModuleCycle(cycle)) - } - exec.moduleLoading[entry.key] = true - exec.moduleLoadStack = append(exec.moduleLoadStack, entry.key) - defer func() { - delete(exec.moduleLoading, entry.key) - if len(exec.moduleLoadStack) > 0 { - exec.moduleLoadStack = exec.moduleLoadStack[:len(exec.moduleLoadStack)-1] - } - }() - - moduleEnv := newEnv(exec.root) - exports := make(map[string]Value, len(entry.script.functions)) - for name, fn := range entry.script.functions { - clone := cloneFunctionForEnv(fn, moduleEnv) - fnVal := NewFunction(clone) - moduleEnv.Define(name, fnVal) - if shouldExportModuleFunction(fn) { - exports[name] = fnVal - } - } - - exportsVal := NewObject(exports) - if err := validateRequireAliasBinding(exec.root, alias, exportsVal); err != nil { - return NewNil(), err - } - bindModuleExportsWithoutOverwrite(exec.root, exports) - exec.modules[entry.key] = exportsVal - if alias != "" { - exec.root.Define(alias, exportsVal) - } - return exportsVal, nil -} diff --git a/vibes/modules_require.go b/vibes/modules_require.go new file mode 100644 index 0000000..e3cd3c4 --- /dev/null +++ b/vibes/modules_require.go @@ -0,0 +1,234 @@ +package vibes + +import ( + "fmt" + "reflect" + "strings" +) + +func moduleCycleFromLoadStack(stack []string, next string) ([]string, bool) { + for idx, key := range stack { + if key == next { + cycle := append(append([]string(nil), stack[idx:]...), next) + return cycle, true + } + } + return nil, false +} + +func moduleExecutionChain(stack []moduleContext) []string { + chain := make([]string, 0, len(stack)) + for _, ctx := range stack { + if ctx.key == "" { + continue + } + if len(chain) > 0 && chain[len(chain)-1] == ctx.key { + continue + } + chain = append(chain, ctx.key) + } + return chain +} + +func moduleCycleFromExecution(stack []moduleContext, next string) ([]string, bool) { + chain := moduleExecutionChain(stack) + if len(chain) < 2 { + return nil, false + } + for idx, key := range chain[:len(chain)-1] { + if key == next { + cycle := append(append([]string(nil), chain[idx:]...), next) + return cycle, true + } + } + return nil, false +} + +func formatModuleCycle(cycle []string) string { + if len(cycle) == 0 { + return "" + } + normalized := make([]string, 0, len(cycle)) + for _, key := range cycle { + if len(normalized) > 0 && normalized[len(normalized)-1] == key { + continue + } + normalized = append(normalized, key) + } + parts := make([]string, len(normalized)) + for idx, key := range normalized { + parts[idx] = moduleDisplayName(key) + } + return strings.Join(parts, " -> ") +} + +func shouldExportModuleFunction(fn *ScriptFunction) bool { + return fn != nil && !fn.Private +} + +func parseRequireAlias(kwargs map[string]Value) (string, error) { + if len(kwargs) == 0 { + return "", nil + } + if len(kwargs) != 1 { + for key := range kwargs { + if key != "as" { + return "", fmt.Errorf("require: unknown keyword argument %s", key) + } + } + return "", fmt.Errorf("require: unknown keyword arguments") + } + + aliasVal, ok := kwargs["as"] + if !ok { + for key := range kwargs { + return "", fmt.Errorf("require: unknown keyword argument %s", key) + } + return "", fmt.Errorf("require: unknown keyword arguments") + } + + var aliasName string + switch aliasVal.Kind() { + case KindString, KindSymbol: + aliasName = strings.TrimSpace(aliasVal.String()) + default: + return "", fmt.Errorf("require: alias must be a string or symbol") + } + + if !isValidModuleAlias(aliasName) { + return "", fmt.Errorf("require: invalid alias %q", aliasName) + } + + return aliasName, nil +} + +func isValidModuleAlias(name string) bool { + if name == "" { + return false + } + runes := []rune(name) + if len(runes) == 0 || !isIdentifierStart(runes[0]) { + return false + } + for _, r := range runes[1:] { + if !isIdentifierRune(r) { + return false + } + } + return lookupIdent(name) == tokenIdent +} + +func bindRequireAlias(root *Env, alias string, module Value) error { + if err := validateRequireAliasBinding(root, alias, module); err != nil { + return err + } + if alias == "" { + return nil + } + root.Define(alias, module) + return nil +} + +func validateRequireAliasBinding(root *Env, alias string, module Value) error { + if alias == "" { + return nil + } + if existing, ok := root.Get(alias); ok { + if existing.Kind() == KindObject && module.Kind() == KindObject && reflect.ValueOf(existing.Hash()).Pointer() == reflect.ValueOf(module.Hash()).Pointer() { + return nil + } + return fmt.Errorf("require: alias %q already defined", alias) + } + return nil +} + +func bindModuleExportsWithoutOverwrite(root *Env, exports map[string]Value) { + for name, fnVal := range exports { + if _, exists := root.Get(name); exists { + continue + } + root.Define(name, fnVal) + } +} + +func builtinRequire(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if exec.strictEffects && !exec.allowRequire { + return NewNil(), fmt.Errorf("strict effects: require is disabled without CallOptions.AllowRequire") + } + if len(args) != 1 { + return NewNil(), fmt.Errorf("require expects a single module name argument") + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("require does not accept blocks") + } + if exec.root == nil { + return NewNil(), fmt.Errorf("require unavailable in this context") + } + alias, err := parseRequireAlias(kwargs) + if err != nil { + return NewNil(), err + } + + modNameVal := args[0] + switch modNameVal.Kind() { + case KindString, KindSymbol: + // supported + default: + return NewNil(), fmt.Errorf("require expects a string or symbol module name") + } + + entry, err := exec.engine.loadModule(modNameVal.String(), exec.currentModuleContext()) + if err != nil { + return NewNil(), err + } + + if cycle, ok := moduleCycleFromLoadStack(exec.moduleLoadStack, entry.key); ok { + return NewNil(), fmt.Errorf("require: circular dependency detected: %s", formatModuleCycle(cycle)) + } + + if cached, ok := exec.modules[entry.key]; ok { + if err := bindRequireAlias(exec.root, alias, cached); err != nil { + return NewNil(), err + } + return cached, nil + } + + if cycle, ok := moduleCycleFromExecution(exec.moduleStack, entry.key); ok { + return NewNil(), fmt.Errorf("require: circular dependency detected: %s", formatModuleCycle(cycle)) + } + + if exec.moduleLoading[entry.key] { + cycle := append(append([]string(nil), exec.moduleLoadStack...), entry.key) + return NewNil(), fmt.Errorf("require: circular dependency detected: %s", formatModuleCycle(cycle)) + } + exec.moduleLoading[entry.key] = true + exec.moduleLoadStack = append(exec.moduleLoadStack, entry.key) + defer func() { + delete(exec.moduleLoading, entry.key) + if len(exec.moduleLoadStack) > 0 { + exec.moduleLoadStack = exec.moduleLoadStack[:len(exec.moduleLoadStack)-1] + } + }() + + moduleEnv := newEnv(exec.root) + exports := make(map[string]Value, len(entry.script.functions)) + for name, fn := range entry.script.functions { + clone := cloneFunctionForEnv(fn, moduleEnv) + fnVal := NewFunction(clone) + moduleEnv.Define(name, fnVal) + if shouldExportModuleFunction(fn) { + exports[name] = fnVal + } + } + + exportsVal := NewObject(exports) + if err := validateRequireAliasBinding(exec.root, alias, exportsVal); err != nil { + return NewNil(), err + } + bindModuleExportsWithoutOverwrite(exec.root, exports) + exec.modules[entry.key] = exportsVal + if alias != "" { + exec.root.Define(alias, exportsVal) + } + return exportsVal, nil +} From d40649e312d4cc4beab403e8974bc2f0ca83ab6a Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 21:56:05 -0500 Subject: [PATCH 05/99] Move class integration coverage into dedicated test file --- vibes/integration_classes_test.go | 57 +++++++++++++++++++++++++++++++ vibes/integration_test.go | 50 --------------------------- 2 files changed, 57 insertions(+), 50 deletions(-) create mode 100644 vibes/integration_classes_test.go diff --git a/vibes/integration_classes_test.go b/vibes/integration_classes_test.go new file mode 100644 index 0000000..d00f297 --- /dev/null +++ b/vibes/integration_classes_test.go @@ -0,0 +1,57 @@ +package vibes + +import ( + "context" + "strings" + "testing" +) + +func TestClassPrivacyEnforced(t *testing.T) { + script := compileTestProgram(t, "classes/privacy.vibe") + _, err := script.Call(context.Background(), "violate", nil, CallOptions{}) + if err == nil { + t.Fatalf("expected privacy violation") + } + if !strings.Contains(err.Error(), "private method secret") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestClassErrorCases(t *testing.T) { + script := compileTestProgram(t, "errors/classes.vibe") + + checkErr := func(fn, contains string) { + t.Helper() + _, err := script.Call(context.Background(), fn, nil, CallOptions{}) + if err == nil { + t.Fatalf("%s: expected error", fn) + } + if !strings.Contains(err.Error(), contains) { + t.Fatalf("%s: unexpected error '%v', want '%s'", fn, err, contains) + } + } + + checkErr("undefined_method", "unknown") + checkErr("private_method_external", "private method") + checkErr("write_to_readonly", "read-only property") + checkErr("wrong_init_args", "argument") + + // run function should work + val, err := script.Call(context.Background(), "run", nil, CallOptions{}) + if err != nil { + t.Fatalf("run: unexpected error: %v", err) + } + if val.Kind() != KindHash { + t.Fatalf("run: expected hash, got %v", val.Kind()) + } + h := val.Hash() + if h["counter"].Int() != 7 { + t.Fatalf("run: counter mismatch: %v", h["counter"]) + } + if h["readonly"].String() != "hello" { + t.Fatalf("run: readonly mismatch: %v", h["readonly"]) + } + if h["writeonly"].Int() != 99 { + t.Fatalf("run: writeonly mismatch: %v", h["writeonly"]) + } +} diff --git a/vibes/integration_test.go b/vibes/integration_test.go index 45dcd88..6ed85ec 100644 --- a/vibes/integration_test.go +++ b/vibes/integration_test.go @@ -430,17 +430,6 @@ func TestProgramFixtures(t *testing.T) { } } -func TestClassPrivacyEnforced(t *testing.T) { - script := compileTestProgram(t, "classes/privacy.vibe") - _, err := script.Call(context.Background(), "violate", nil, CallOptions{}) - if err == nil { - t.Fatalf("expected privacy violation") - } - if !strings.Contains(err.Error(), "private method secret") { - t.Fatalf("unexpected error: %v", err) - } -} - func TestBlockErrorCases(t *testing.T) { script := compileTestProgram(t, "blocks/error_cases.vibe") @@ -644,45 +633,6 @@ func TestYieldErrorCases(t *testing.T) { })) } -func TestClassErrorCases(t *testing.T) { - script := compileTestProgram(t, "errors/classes.vibe") - - checkErr := func(fn, contains string) { - t.Helper() - _, err := script.Call(context.Background(), fn, nil, CallOptions{}) - if err == nil { - t.Fatalf("%s: expected error", fn) - } - if !strings.Contains(err.Error(), contains) { - t.Fatalf("%s: unexpected error '%v', want '%s'", fn, err, contains) - } - } - - checkErr("undefined_method", "unknown") - checkErr("private_method_external", "private method") - checkErr("write_to_readonly", "read-only property") - checkErr("wrong_init_args", "argument") - - // run function should work - val, err := script.Call(context.Background(), "run", nil, CallOptions{}) - if err != nil { - t.Fatalf("run: unexpected error: %v", err) - } - if val.Kind() != KindHash { - t.Fatalf("run: expected hash, got %v", val.Kind()) - } - h := val.Hash() - if h["counter"].Int() != 7 { - t.Fatalf("run: counter mismatch: %v", h["counter"]) - } - if h["readonly"].String() != "hello" { - t.Fatalf("run: readonly mismatch: %v", h["readonly"]) - } - if h["writeonly"].Int() != 99 { - t.Fatalf("run: writeonly mismatch: %v", h["writeonly"]) - } -} - func TestArgumentErrorCases(t *testing.T) { script := compileTestProgram(t, "errors/arguments.vibe") From 6dd315f6dc062d34be8e8903179eb9a20743d2ac Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 21:56:41 -0500 Subject: [PATCH 06/99] Add internal architecture guide for runtime parser and modules --- README.md | 1 + docs/architecture.md | 73 ++++++++++++++++++++++++++++++++++++++++++++ docs/introduction.md | 1 + 3 files changed, 75 insertions(+) create mode 100644 docs/architecture.md diff --git a/README.md b/README.md index bf9ce14..92cc463 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ Long-form guides live in `docs/`: - `docs/control-flow.md` – conditionals, loops, and ranges. - `docs/blocks.md` – working with block literals for enumerable-style operations. - `docs/tooling.md` – CLI workflows for running, formatting, analyzing, language-server usage, and REPL usage. +- `docs/architecture.md` – internal runtime/parser/module architecture notes for maintainers. - `docs/integration.md` – integrating the interpreter in Go applications. - `docs/host_cookbook.md` – production integration patterns for embedding hosts. - `docs/starter_templates.md` – starter scaffolds for common embedding scenarios. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..452055b --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,73 @@ +# Internal Architecture + +This document summarizes how core interpreter subsystems fit together. + +## Execution Flow + +High-level call path: + +1. `Script.Call(...)` clones function/class declarations into an isolated call environment. +2. Builtins, globals, capabilities, and module context are bound into root env. +3. Class bodies are evaluated to initialize class variables. +4. Target function arguments are bound and type-checked. +5. Statement/expression evaluators execute the script and enforce: + - step quota + - recursion limit + - memory quota + +Key files: + +- `vibes/execution.go` (core evaluator, call orchestration) +- `vibes/execution_types.go` (type-checking + type formatting helpers) +- `vibes/execution_values.go` (value conversion, arithmetic, comparison helpers) + +## Parsing And AST + +Pipeline: + +1. `lexer` tokenizes source. +2. `parser` builds AST statements/expressions. +3. `Engine.Compile(...)` lowers AST declarations into `ScriptFunction` and `ClassDef`. + +Key files: + +- `vibes/lexer.go` +- `vibes/parser.go` (parser core + precedence + token/error helpers) +- `vibes/parser_statements.go` (statement-level parsing) +- `vibes/parser_types.go` (type-expression parsing) +- `vibes/ast.go` + +## Modules (`require`) + +`require` runtime behavior: + +1. Parse module request and optional alias. +2. Resolve relative or search-path module file. +3. Enforce allow/deny policy rules. +4. Compile + cache module script by normalized cache key. +5. Execute module in a module-local env. +6. Export non-private functions to module object. +7. Inject non-conflicting exports into globals and optionally bind alias. + +Key files: + +- `vibes/modules.go` (module request parsing, path resolution, policy, cache/load) +- `vibes/modules_require.go` (runtime require execution, export/alias behavior, cycle reporting) + +## Builtins + +Builtins are registered during engine initialization: + +- core registration entrypoint: `registerCoreBuiltins(...)` in `vibes/interpreter.go` +- domain files: + - `vibes/builtins.go` (core/id helpers) + - `vibes/builtins_numeric.go` + - `vibes/builtins_json_regex.go` + +## Refactor Constraints + +When refactoring internals: + +- Preserve runtime error text when possible (tests assert key messages). +- Keep parser behavior stable unless paired with migration/docs updates. +- Run `go test ./...` and style gates after every atomic change. diff --git a/docs/introduction.md b/docs/introduction.md index f849e3d..5b804c7 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -26,6 +26,7 @@ dives on specific topics. - `control-flow.md` – conditionals, loops, and ranges. - `blocks.md` – using block literals for map/select/reduce style patterns. - `tooling.md` – CLI commands for run/format/analyze/repl workflows. +- `architecture.md` – internal runtime/parser/module architecture map for maintainers. - `integration.md` – host integration patterns showing how Go services can expose capabilities to scripts. - `host_cookbook.md` – production embedding patterns and operational guidance. From f8ede8138b18ae08525a64f780687561c31dc28e Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:04:08 -0500 Subject: [PATCH 07/99] Extract compile and script call orchestration from execution core --- vibes/execution.go | 371 ------------------------------------- vibes/execution_script.go | 380 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+), 371 deletions(-) create mode 100644 vibes/execution_script.go diff --git a/vibes/execution.go b/vibes/execution.go index aa9b652..01bc954 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -3839,374 +3839,3 @@ func arrayMember(array Value, property string) (Value, error) { return NewNil(), fmt.Errorf("unknown array method %s", property) } } - -func (e *Engine) Compile(source string) (*Script, error) { - p := newParser(source) - program, parseErrors := p.ParseProgram() - if len(parseErrors) > 0 { - return nil, combineErrors(parseErrors) - } - - functions := make(map[string]*ScriptFunction) - classes := make(map[string]*ClassDef) - - for _, stmt := range program.Statements { - switch s := stmt.(type) { - case *FunctionStmt: - if _, exists := functions[s.Name]; exists { - return nil, fmt.Errorf("duplicate function %s", s.Name) - } - functions[s.Name] = &ScriptFunction{Name: s.Name, Params: s.Params, ReturnTy: s.ReturnTy, Body: s.Body, Pos: s.Pos(), Exported: s.Exported, Private: s.Private} - case *ClassStmt: - if _, exists := classes[s.Name]; exists { - return nil, fmt.Errorf("duplicate class %s", s.Name) - } - classDef := &ClassDef{ - Name: s.Name, - Methods: make(map[string]*ScriptFunction), - ClassMethods: make(map[string]*ScriptFunction), - ClassVars: make(map[string]Value), - Body: s.Body, - } - for _, prop := range s.Properties { - for _, name := range prop.Names { - if prop.Kind == "property" || prop.Kind == "getter" { - getter := &ScriptFunction{ - Name: name, - Body: []Statement{&ReturnStmt{Value: &IvarExpr{Name: name, position: prop.position}, position: prop.position}}, - Pos: prop.position, - } - classDef.Methods[name] = getter - } - if prop.Kind == "property" || prop.Kind == "setter" { - setter := &ScriptFunction{ - Name: name + "=", - Params: []Param{{ - Name: "value", - }}, - Body: []Statement{ - &AssignStmt{ - Target: &IvarExpr{Name: name, position: prop.position}, - Value: &Identifier{Name: "value", position: prop.position}, - position: prop.position, - }, - &ReturnStmt{Value: &Identifier{Name: "value", position: prop.position}, position: prop.position}, - }, - Pos: prop.position, - } - classDef.Methods[name+"="] = setter - } - } - } - for _, fn := range s.Methods { - classDef.Methods[fn.Name] = &ScriptFunction{Name: fn.Name, Params: fn.Params, ReturnTy: fn.ReturnTy, Body: fn.Body, Pos: fn.Pos(), Private: fn.Private} - } - for _, fn := range s.ClassMethods { - classDef.ClassMethods[fn.Name] = &ScriptFunction{Name: fn.Name, Params: fn.Params, ReturnTy: fn.ReturnTy, Body: fn.Body, Pos: fn.Pos(), Private: fn.Private} - } - classes[s.Name] = classDef - default: - return nil, fmt.Errorf("unsupported top-level statement %T", stmt) - } - } - - script := &Script{engine: e, functions: functions, classes: classes, source: source} - script.bindFunctionOwnership() - return script, nil -} - -func combineErrors(errs []error) error { - if len(errs) == 1 { - return errs[0] - } - msg := "" - for _, err := range errs { - if msg != "" { - msg += "\n\n" - } - msg += err.Error() - } - return errors.New(msg) -} - -func (exec *Execution) bindFunctionArgs(fn *ScriptFunction, env *Env, args []Value, kwargs map[string]Value, pos Position) error { - usedKw := make(map[string]bool, len(kwargs)) - argIdx := 0 - - for _, param := range fn.Params { - var val Value - if argIdx < len(args) { - val = args[argIdx] - argIdx++ - } else if kw, ok := kwargs[param.Name]; ok { - val = kw - usedKw[param.Name] = true - } else if param.DefaultVal != nil { - defaultVal, err := exec.evalExpressionWithAuto(param.DefaultVal, env, true) - if err != nil { - return err - } - val = defaultVal - } else { - return exec.errorAt(pos, "missing argument %s", param.Name) - } - - if param.Type != nil { - if err := checkValueType(val, param.Type); err != nil { - return exec.errorAt(pos, "%s", formatArgumentTypeMismatch(param.Name, err)) - } - } - env.Define(param.Name, val) - if param.IsIvar { - if selfVal, ok := env.Get("self"); ok && selfVal.Kind() == KindInstance { - inst := selfVal.Instance() - if inst != nil { - inst.Ivars[param.Name] = val - } - } - } - } - - if argIdx < len(args) { - return exec.errorAt(pos, "unexpected positional arguments") - } - for name := range kwargs { - if !usedKw[name] { - return exec.errorAt(pos, "unexpected keyword argument %s", name) - } - } - return nil -} - -// Function looks up a compiled function by name. -func (s *Script) Function(name string) (*ScriptFunction, bool) { - fn, ok := s.functions[name] - return fn, ok -} - -// Functions returns compiled functions in deterministic name order. -func (s *Script) Functions() []*ScriptFunction { - names := make([]string, 0, len(s.functions)) - for name := range s.functions { - names = append(names, name) - } - sort.Strings(names) - out := make([]*ScriptFunction, 0, len(names)) - for _, name := range names { - out = append(out, s.functions[name]) - } - return out -} - -// Classes returns compiled classes in deterministic name order. -func (s *Script) Classes() []*ClassDef { - names := make([]string, 0, len(s.classes)) - for name := range s.classes { - names = append(names, name) - } - sort.Strings(names) - out := make([]*ClassDef, 0, len(names)) - for _, name := range names { - out = append(out, s.classes[name]) - } - return out -} - -func (s *Script) bindFunctionOwnership() { - for _, fn := range s.functions { - fn.owner = s - } - for _, classDef := range s.classes { - classDef.owner = s - for _, fn := range classDef.Methods { - fn.owner = s - } - for _, fn := range classDef.ClassMethods { - fn.owner = s - } - } -} - -func cloneFunctionsForCall(functions map[string]*ScriptFunction, env *Env) map[string]*ScriptFunction { - cloned := make(map[string]*ScriptFunction, len(functions)) - for name, fn := range functions { - cloned[name] = cloneFunctionForEnv(fn, env) - } - return cloned -} - -func cloneClassesForCall(classes map[string]*ClassDef, env *Env) map[string]*ClassDef { - cloned := make(map[string]*ClassDef, len(classes)) - for name, classDef := range classes { - classClone := &ClassDef{ - Name: classDef.Name, - Methods: make(map[string]*ScriptFunction, len(classDef.Methods)), - ClassMethods: make(map[string]*ScriptFunction, len(classDef.ClassMethods)), - ClassVars: make(map[string]Value), - Body: classDef.Body, - owner: classDef.owner, - } - for methodName, method := range classDef.Methods { - classClone.Methods[methodName] = cloneFunctionForEnv(method, env) - } - for methodName, method := range classDef.ClassMethods { - classClone.ClassMethods[methodName] = cloneFunctionForEnv(method, env) - } - cloned[name] = classClone - } - return cloned -} - -func (s *Script) Call(ctx context.Context, name string, args []Value, opts CallOptions) (Value, error) { - if ctx == nil { - ctx = context.Background() - } - - _, ok := s.functions[name] - if !ok { - return NewNil(), fmt.Errorf("function %s not found", name) - } - - root := newEnv(nil) - for n, builtin := range s.engine.builtins { - root.Define(n, builtin) - } - - callFunctions := cloneFunctionsForCall(s.functions, root) - fn, ok := callFunctions[name] - if !ok { - return NewNil(), fmt.Errorf("function %s not found", name) - } - for n, fnDecl := range callFunctions { - root.Define(n, NewFunction(fnDecl)) - } - - callClasses := cloneClassesForCall(s.classes, root) - for n, classDef := range callClasses { - root.Define(n, NewClass(classDef)) - } - rebinder := newCallFunctionRebinder(s, root, callClasses) - - exec := &Execution{ - engine: s.engine, - script: s, - ctx: ctx, - quota: s.engine.config.StepQuota, - memoryQuota: s.engine.config.MemoryQuotaBytes, - recursionCap: s.engine.config.RecursionLimit, - callStack: make([]callFrame, 0, 8), - root: root, - modules: make(map[string]Value), - moduleLoading: make(map[string]bool), - moduleLoadStack: make([]string, 0, 8), - moduleStack: make([]moduleContext, 0, 8), - capabilityContracts: make(map[*Builtin]CapabilityMethodContract), - capabilityContractScopes: make(map[*Builtin]*capabilityContractScope), - capabilityContractsByName: make(map[string]CapabilityMethodContract), - receiverStack: make([]Value, 0, 8), - envStack: make([]*Env, 0, 8), - strictEffects: s.engine.config.StrictEffects, - allowRequire: opts.AllowRequire, - } - - if len(opts.Capabilities) > 0 { - binding := CapabilityBinding{Context: exec.ctx, Engine: s.engine} - for _, adapter := range opts.Capabilities { - if adapter == nil { - continue - } - scope := &capabilityContractScope{ - contracts: map[string]CapabilityMethodContract{}, - } - if provider, ok := adapter.(CapabilityContractProvider); ok { - for methodName, contract := range provider.CapabilityContracts() { - name := strings.TrimSpace(methodName) - if name == "" { - return NewNil(), fmt.Errorf("capability contract method name must be non-empty") - } - if _, exists := exec.capabilityContractsByName[name]; exists { - return NewNil(), fmt.Errorf("duplicate capability contract for %s", name) - } - exec.capabilityContractsByName[name] = contract - scope.contracts[name] = contract - } - } - globals, err := adapter.Bind(binding) - if err != nil { - return NewNil(), err - } - for name, val := range globals { - rebound := rebinder.rebindValue(val) - root.Define(name, rebound) - if len(scope.contracts) > 0 { - scope.roots = append(scope.roots, rebound) - } - bindCapabilityContracts(rebound, scope, exec.capabilityContracts, exec.capabilityContractScopes) - } - } - } - - if exec.strictEffects { - if err := validateStrictGlobals(opts.Globals); err != nil { - return NewNil(), err - } - } - - for n, val := range opts.Globals { - root.Define(n, rebinder.rebindValue(val)) - } - - if err := exec.checkMemory(); err != nil { - return NewNil(), err - } - - // initialize class bodies (class vars) - for name, classDef := range callClasses { - if len(classDef.Body) == 0 { - continue - } - classVal, _ := root.Get(name) - env := newEnv(root) - env.Define("self", classVal) - exec.pushReceiver(classVal) - _, _, err := exec.evalStatements(classDef.Body, env) - exec.popReceiver() - if err != nil { - return NewNil(), err - } - } - - callEnv := newEnv(root) - callArgs := rebinder.rebindValues(args) - callKeywords := rebinder.rebindKeywords(opts.Keywords) - if err := exec.bindFunctionArgs(fn, callEnv, callArgs, callKeywords, fn.Pos); err != nil { - return NewNil(), err - } - exec.pushEnv(callEnv) - if err := exec.checkMemory(); err != nil { - exec.popEnv() - return NewNil(), err - } - exec.popEnv() - - if err := exec.pushFrame(fn.Name, fn.Pos); err != nil { - return NewNil(), err - } - val, returned, err := exec.evalStatements(fn.Body, callEnv) - exec.popFrame() - if err != nil { - return NewNil(), err - } - if fn.ReturnTy != nil { - if err := checkValueType(val, fn.ReturnTy); err != nil { - return NewNil(), exec.errorAt(fn.Pos, "%s", formatReturnTypeMismatch(fn.Name, err)) - } - } - if err := exec.checkMemoryWith(val); err != nil { - return NewNil(), err - } - if returned { - return val, nil - } - return val, nil -} diff --git a/vibes/execution_script.go b/vibes/execution_script.go new file mode 100644 index 0000000..f46c2e5 --- /dev/null +++ b/vibes/execution_script.go @@ -0,0 +1,380 @@ +package vibes + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" +) + +func (e *Engine) Compile(source string) (*Script, error) { + p := newParser(source) + program, parseErrors := p.ParseProgram() + if len(parseErrors) > 0 { + return nil, combineErrors(parseErrors) + } + + functions := make(map[string]*ScriptFunction) + classes := make(map[string]*ClassDef) + + for _, stmt := range program.Statements { + switch s := stmt.(type) { + case *FunctionStmt: + if _, exists := functions[s.Name]; exists { + return nil, fmt.Errorf("duplicate function %s", s.Name) + } + functions[s.Name] = &ScriptFunction{Name: s.Name, Params: s.Params, ReturnTy: s.ReturnTy, Body: s.Body, Pos: s.Pos(), Exported: s.Exported, Private: s.Private} + case *ClassStmt: + if _, exists := classes[s.Name]; exists { + return nil, fmt.Errorf("duplicate class %s", s.Name) + } + classDef := &ClassDef{ + Name: s.Name, + Methods: make(map[string]*ScriptFunction), + ClassMethods: make(map[string]*ScriptFunction), + ClassVars: make(map[string]Value), + Body: s.Body, + } + for _, prop := range s.Properties { + for _, name := range prop.Names { + if prop.Kind == "property" || prop.Kind == "getter" { + getter := &ScriptFunction{ + Name: name, + Body: []Statement{&ReturnStmt{Value: &IvarExpr{Name: name, position: prop.position}, position: prop.position}}, + Pos: prop.position, + } + classDef.Methods[name] = getter + } + if prop.Kind == "property" || prop.Kind == "setter" { + setter := &ScriptFunction{ + Name: name + "=", + Params: []Param{{ + Name: "value", + }}, + Body: []Statement{ + &AssignStmt{ + Target: &IvarExpr{Name: name, position: prop.position}, + Value: &Identifier{Name: "value", position: prop.position}, + position: prop.position, + }, + &ReturnStmt{Value: &Identifier{Name: "value", position: prop.position}, position: prop.position}, + }, + Pos: prop.position, + } + classDef.Methods[name+"="] = setter + } + } + } + for _, fn := range s.Methods { + classDef.Methods[fn.Name] = &ScriptFunction{Name: fn.Name, Params: fn.Params, ReturnTy: fn.ReturnTy, Body: fn.Body, Pos: fn.Pos(), Private: fn.Private} + } + for _, fn := range s.ClassMethods { + classDef.ClassMethods[fn.Name] = &ScriptFunction{Name: fn.Name, Params: fn.Params, ReturnTy: fn.ReturnTy, Body: fn.Body, Pos: fn.Pos(), Private: fn.Private} + } + classes[s.Name] = classDef + default: + return nil, fmt.Errorf("unsupported top-level statement %T", stmt) + } + } + + script := &Script{engine: e, functions: functions, classes: classes, source: source} + script.bindFunctionOwnership() + return script, nil +} + +func combineErrors(errs []error) error { + if len(errs) == 1 { + return errs[0] + } + msg := "" + for _, err := range errs { + if msg != "" { + msg += "\n\n" + } + msg += err.Error() + } + return errors.New(msg) +} + +func (exec *Execution) bindFunctionArgs(fn *ScriptFunction, env *Env, args []Value, kwargs map[string]Value, pos Position) error { + usedKw := make(map[string]bool, len(kwargs)) + argIdx := 0 + + for _, param := range fn.Params { + var val Value + if argIdx < len(args) { + val = args[argIdx] + argIdx++ + } else if kw, ok := kwargs[param.Name]; ok { + val = kw + usedKw[param.Name] = true + } else if param.DefaultVal != nil { + defaultVal, err := exec.evalExpressionWithAuto(param.DefaultVal, env, true) + if err != nil { + return err + } + val = defaultVal + } else { + return exec.errorAt(pos, "missing argument %s", param.Name) + } + + if param.Type != nil { + if err := checkValueType(val, param.Type); err != nil { + return exec.errorAt(pos, "%s", formatArgumentTypeMismatch(param.Name, err)) + } + } + env.Define(param.Name, val) + if param.IsIvar { + if selfVal, ok := env.Get("self"); ok && selfVal.Kind() == KindInstance { + inst := selfVal.Instance() + if inst != nil { + inst.Ivars[param.Name] = val + } + } + } + } + + if argIdx < len(args) { + return exec.errorAt(pos, "unexpected positional arguments") + } + for name := range kwargs { + if !usedKw[name] { + return exec.errorAt(pos, "unexpected keyword argument %s", name) + } + } + return nil +} + +// Function looks up a compiled function by name. +func (s *Script) Function(name string) (*ScriptFunction, bool) { + fn, ok := s.functions[name] + return fn, ok +} + +// Functions returns compiled functions in deterministic name order. +func (s *Script) Functions() []*ScriptFunction { + names := make([]string, 0, len(s.functions)) + for name := range s.functions { + names = append(names, name) + } + sort.Strings(names) + out := make([]*ScriptFunction, 0, len(names)) + for _, name := range names { + out = append(out, s.functions[name]) + } + return out +} + +// Classes returns compiled classes in deterministic name order. +func (s *Script) Classes() []*ClassDef { + names := make([]string, 0, len(s.classes)) + for name := range s.classes { + names = append(names, name) + } + sort.Strings(names) + out := make([]*ClassDef, 0, len(names)) + for _, name := range names { + out = append(out, s.classes[name]) + } + return out +} + +func (s *Script) bindFunctionOwnership() { + for _, fn := range s.functions { + fn.owner = s + } + for _, classDef := range s.classes { + classDef.owner = s + for _, fn := range classDef.Methods { + fn.owner = s + } + for _, fn := range classDef.ClassMethods { + fn.owner = s + } + } +} + +func cloneFunctionsForCall(functions map[string]*ScriptFunction, env *Env) map[string]*ScriptFunction { + cloned := make(map[string]*ScriptFunction, len(functions)) + for name, fn := range functions { + cloned[name] = cloneFunctionForEnv(fn, env) + } + return cloned +} + +func cloneClassesForCall(classes map[string]*ClassDef, env *Env) map[string]*ClassDef { + cloned := make(map[string]*ClassDef, len(classes)) + for name, classDef := range classes { + classClone := &ClassDef{ + Name: classDef.Name, + Methods: make(map[string]*ScriptFunction, len(classDef.Methods)), + ClassMethods: make(map[string]*ScriptFunction, len(classDef.ClassMethods)), + ClassVars: make(map[string]Value), + Body: classDef.Body, + owner: classDef.owner, + } + for methodName, method := range classDef.Methods { + classClone.Methods[methodName] = cloneFunctionForEnv(method, env) + } + for methodName, method := range classDef.ClassMethods { + classClone.ClassMethods[methodName] = cloneFunctionForEnv(method, env) + } + cloned[name] = classClone + } + return cloned +} + +func (s *Script) Call(ctx context.Context, name string, args []Value, opts CallOptions) (Value, error) { + if ctx == nil { + ctx = context.Background() + } + + _, ok := s.functions[name] + if !ok { + return NewNil(), fmt.Errorf("function %s not found", name) + } + + root := newEnv(nil) + for n, builtin := range s.engine.builtins { + root.Define(n, builtin) + } + + callFunctions := cloneFunctionsForCall(s.functions, root) + fn, ok := callFunctions[name] + if !ok { + return NewNil(), fmt.Errorf("function %s not found", name) + } + for n, fnDecl := range callFunctions { + root.Define(n, NewFunction(fnDecl)) + } + + callClasses := cloneClassesForCall(s.classes, root) + for n, classDef := range callClasses { + root.Define(n, NewClass(classDef)) + } + rebinder := newCallFunctionRebinder(s, root, callClasses) + + exec := &Execution{ + engine: s.engine, + script: s, + ctx: ctx, + quota: s.engine.config.StepQuota, + memoryQuota: s.engine.config.MemoryQuotaBytes, + recursionCap: s.engine.config.RecursionLimit, + callStack: make([]callFrame, 0, 8), + root: root, + modules: make(map[string]Value), + moduleLoading: make(map[string]bool), + moduleLoadStack: make([]string, 0, 8), + moduleStack: make([]moduleContext, 0, 8), + capabilityContracts: make(map[*Builtin]CapabilityMethodContract), + capabilityContractScopes: make(map[*Builtin]*capabilityContractScope), + capabilityContractsByName: make(map[string]CapabilityMethodContract), + receiverStack: make([]Value, 0, 8), + envStack: make([]*Env, 0, 8), + strictEffects: s.engine.config.StrictEffects, + allowRequire: opts.AllowRequire, + } + + if len(opts.Capabilities) > 0 { + binding := CapabilityBinding{Context: exec.ctx, Engine: s.engine} + for _, adapter := range opts.Capabilities { + if adapter == nil { + continue + } + scope := &capabilityContractScope{ + contracts: map[string]CapabilityMethodContract{}, + } + if provider, ok := adapter.(CapabilityContractProvider); ok { + for methodName, contract := range provider.CapabilityContracts() { + name := strings.TrimSpace(methodName) + if name == "" { + return NewNil(), fmt.Errorf("capability contract method name must be non-empty") + } + if _, exists := exec.capabilityContractsByName[name]; exists { + return NewNil(), fmt.Errorf("duplicate capability contract for %s", name) + } + exec.capabilityContractsByName[name] = contract + scope.contracts[name] = contract + } + } + globals, err := adapter.Bind(binding) + if err != nil { + return NewNil(), err + } + for name, val := range globals { + rebound := rebinder.rebindValue(val) + root.Define(name, rebound) + if len(scope.contracts) > 0 { + scope.roots = append(scope.roots, rebound) + } + bindCapabilityContracts(rebound, scope, exec.capabilityContracts, exec.capabilityContractScopes) + } + } + } + + if exec.strictEffects { + if err := validateStrictGlobals(opts.Globals); err != nil { + return NewNil(), err + } + } + + for n, val := range opts.Globals { + root.Define(n, rebinder.rebindValue(val)) + } + + if err := exec.checkMemory(); err != nil { + return NewNil(), err + } + + // initialize class bodies (class vars) + for name, classDef := range callClasses { + if len(classDef.Body) == 0 { + continue + } + classVal, _ := root.Get(name) + env := newEnv(root) + env.Define("self", classVal) + exec.pushReceiver(classVal) + _, _, err := exec.evalStatements(classDef.Body, env) + exec.popReceiver() + if err != nil { + return NewNil(), err + } + } + + callEnv := newEnv(root) + callArgs := rebinder.rebindValues(args) + callKeywords := rebinder.rebindKeywords(opts.Keywords) + if err := exec.bindFunctionArgs(fn, callEnv, callArgs, callKeywords, fn.Pos); err != nil { + return NewNil(), err + } + exec.pushEnv(callEnv) + if err := exec.checkMemory(); err != nil { + exec.popEnv() + return NewNil(), err + } + exec.popEnv() + + if err := exec.pushFrame(fn.Name, fn.Pos); err != nil { + return NewNil(), err + } + val, returned, err := exec.evalStatements(fn.Body, callEnv) + exec.popFrame() + if err != nil { + return NewNil(), err + } + if fn.ReturnTy != nil { + if err := checkValueType(val, fn.ReturnTy); err != nil { + return NewNil(), exec.errorAt(fn.Pos, "%s", formatReturnTypeMismatch(fn.Name, err)) + } + } + if err := exec.checkMemoryWith(val); err != nil { + return NewNil(), err + } + if returned { + return val, nil + } + return val, nil +} From 1618f559fb5990330ce7253fb136ec9d7b18b338 Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:05:15 -0500 Subject: [PATCH 08/99] Move runtime member dispatch and helpers into execution_members --- vibes/execution.go | 2322 ----------------------------------- vibes/execution_members.go | 2329 ++++++++++++++++++++++++++++++++++++ 2 files changed, 2329 insertions(+), 2322 deletions(-) create mode 100644 vibes/execution_members.go diff --git a/vibes/execution.go b/vibes/execution.go index 01bc954..348e809 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -4,15 +4,8 @@ import ( "context" "errors" "fmt" - "maps" - "math" - "reflect" "regexp" - "slices" - "sort" "strings" - "time" - "unicode" ) type ScriptFunction struct { @@ -1524,2318 +1517,3 @@ func rescueTypeMatchesErrorKind(ty *TypeExpr, errKind string) bool { } return canonical == errKind } - -func (exec *Execution) getMember(obj Value, property string, pos Position) (Value, error) { - switch obj.Kind() { - case KindHash, KindObject: - if val, ok := obj.Hash()[property]; ok { - return val, nil - } - member, err := hashMember(obj, property) - if err != nil { - return NewNil(), err - } - return member, nil - case KindMoney: - return moneyMember(obj.Money(), property) - case KindDuration: - return durationMember(obj.Duration(), property, pos) - case KindTime: - return timeMember(obj.Time(), property) - case KindArray: - return arrayMember(obj, property) - case KindString: - return stringMember(obj, property) - case KindClass: - cl := obj.Class() - if property == "new" { - return NewAutoBuiltin(cl.Name+".new", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - inst := &Instance{Class: cl, Ivars: make(map[string]Value)} - instVal := NewInstance(inst) - if initFn, ok := cl.Methods["initialize"]; ok { - if _, err := exec.callFunction(initFn, instVal, args, kwargs, block, pos); err != nil { - return NewNil(), err - } - } - return instVal, nil - }), nil - } - if fn, ok := cl.ClassMethods[property]; ok { - if fn.Private && !exec.isCurrentReceiver(obj) { - return NewNil(), exec.errorAt(pos, "private method %s", property) - } - return NewAutoBuiltin(cl.Name+"."+property, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - return exec.callFunction(fn, obj, args, kwargs, block, pos) - }), nil - } - if val, ok := cl.ClassVars[property]; ok { - return val, nil - } - return NewNil(), exec.errorAt(pos, "unknown class member %s", property) - case KindInstance: - inst := obj.Instance() - if property == "class" { - return NewClass(inst.Class), nil - } - if fn, ok := inst.Class.Methods[property]; ok { - if fn.Private && !exec.isCurrentReceiver(obj) { - return NewNil(), exec.errorAt(pos, "private method %s", property) - } - return NewAutoBuiltin(inst.Class.Name+"#"+property, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - return exec.callFunction(fn, obj, args, kwargs, block, pos) - }), nil - } - if val, ok := inst.Ivars[property]; ok { - return val, nil - } - return NewNil(), exec.errorAt(pos, "unknown member %s", property) - case KindInt: - switch property { - case "seconds", "second", "minutes", "minute", "hours", "hour", "days", "day": - return NewDuration(secondsDuration(obj.Int(), property)), nil - case "weeks", "week": - return NewDuration(secondsDuration(obj.Int(), property)), nil - case "abs": - return NewAutoBuiltin("int.abs", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("int.abs does not take arguments") - } - n := receiver.Int() - if n == math.MinInt64 { - return NewNil(), fmt.Errorf("int.abs overflow") - } - if n < 0 { - return NewInt(-n), nil - } - return receiver, nil - }), nil - case "clamp": - return NewAutoBuiltin("int.clamp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("int.clamp expects min and max") - } - if args[0].Kind() != KindInt || args[1].Kind() != KindInt { - return NewNil(), fmt.Errorf("int.clamp expects integer min and max") - } - minVal := args[0].Int() - maxVal := args[1].Int() - if minVal > maxVal { - return NewNil(), fmt.Errorf("int.clamp min must be <= max") - } - n := receiver.Int() - if n < minVal { - return NewInt(minVal), nil - } - if n > maxVal { - return NewInt(maxVal), nil - } - return receiver, nil - }), nil - case "even?": - return NewAutoBuiltin("int.even?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("int.even? does not take arguments") - } - return NewBool(receiver.Int()%2 == 0), nil - }), nil - case "odd?": - return NewAutoBuiltin("int.odd?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("int.odd? does not take arguments") - } - return NewBool(receiver.Int()%2 != 0), nil - }), nil - case "times": - return NewAutoBuiltin("int.times", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("int.times does not take arguments") - } - if block.Block() == nil { - return NewNil(), fmt.Errorf("int.times requires a block") - } - count := receiver.Int() - if count <= 0 { - return receiver, nil - } - if count > int64(math.MaxInt) { - return NewNil(), fmt.Errorf("int.times value too large") - } - for i := range int(count) { - if _, err := exec.CallBlock(block, []Value{NewInt(int64(i))}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - default: - return NewNil(), exec.errorAt(pos, "unknown int member %s", property) - } - case KindFloat: - switch property { - case "abs": - return NewAutoBuiltin("float.abs", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("float.abs does not take arguments") - } - return NewFloat(math.Abs(receiver.Float())), nil - }), nil - case "clamp": - return NewAutoBuiltin("float.clamp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("float.clamp expects min and max") - } - if (args[0].Kind() != KindInt && args[0].Kind() != KindFloat) || (args[1].Kind() != KindInt && args[1].Kind() != KindFloat) { - return NewNil(), fmt.Errorf("float.clamp expects numeric min and max") - } - minVal := args[0].Float() - maxVal := args[1].Float() - if minVal > maxVal { - return NewNil(), fmt.Errorf("float.clamp min must be <= max") - } - n := receiver.Float() - if n < minVal { - return NewFloat(minVal), nil - } - if n > maxVal { - return NewFloat(maxVal), nil - } - return receiver, nil - }), nil - case "round": - return NewAutoBuiltin("float.round", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("float.round does not take arguments") - } - rounded := math.Round(receiver.Float()) - asInt, err := floatToInt64Checked(rounded, "float.round") - if err != nil { - return NewNil(), err - } - return NewInt(asInt), nil - }), nil - case "floor": - return NewAutoBuiltin("float.floor", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("float.floor does not take arguments") - } - floored := math.Floor(receiver.Float()) - asInt, err := floatToInt64Checked(floored, "float.floor") - if err != nil { - return NewNil(), err - } - return NewInt(asInt), nil - }), nil - case "ceil": - return NewAutoBuiltin("float.ceil", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("float.ceil does not take arguments") - } - ceiled := math.Ceil(receiver.Float()) - asInt, err := floatToInt64Checked(ceiled, "float.ceil") - if err != nil { - return NewNil(), err - } - return NewInt(asInt), nil - }), nil - default: - return NewNil(), exec.errorAt(pos, "unknown float member %s", property) - } - default: - return NewNil(), exec.errorAt(pos, "unsupported member access on %s", obj.Kind()) - } -} - -func moneyMember(m Money, property string) (Value, error) { - switch property { - case "currency": - return NewString(m.Currency()), nil - case "cents": - return NewInt(m.Cents()), nil - case "amount": - return NewString(m.String()), nil - case "format": - return NewAutoBuiltin("money.format", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - return NewString(m.String()), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown money member %s", property) - } -} - -func hashMember(obj Value, property string) (Value, error) { - switch property { - case "size": - return NewAutoBuiltin("hash.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.size does not take arguments") - } - return NewInt(int64(len(receiver.Hash()))), nil - }), nil - case "length": - return NewAutoBuiltin("hash.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.length does not take arguments") - } - return NewInt(int64(len(receiver.Hash()))), nil - }), nil - case "empty?": - return NewAutoBuiltin("hash.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.empty? does not take arguments") - } - return NewBool(len(receiver.Hash()) == 0), nil - }), nil - case "key?", "has_key?", "include?": - name := property - return NewAutoBuiltin("hash."+name, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("hash.%s expects exactly one key", name) - } - key, err := valueToHashKey(args[0]) - if err != nil { - return NewNil(), fmt.Errorf("hash.%s key must be symbol or string", name) - } - _, ok := receiver.Hash()[key] - return NewBool(ok), nil - }), nil - case "keys": - return NewAutoBuiltin("hash.keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.keys does not take arguments") - } - keys := sortedHashKeys(receiver.Hash()) - values := make([]Value, len(keys)) - for i, k := range keys { - values[i] = NewSymbol(k) - } - return NewArray(values), nil - }), nil - case "values": - return NewAutoBuiltin("hash.values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.values does not take arguments") - } - entries := receiver.Hash() - keys := sortedHashKeys(entries) - values := make([]Value, len(keys)) - for i, k := range keys { - values[i] = entries[k] - } - return NewArray(values), nil - }), nil - case "fetch": - return NewAutoBuiltin("hash.fetch", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("hash.fetch expects key and optional default") - } - key, err := valueToHashKey(args[0]) - if err != nil { - return NewNil(), fmt.Errorf("hash.fetch key must be symbol or string") - } - if value, ok := receiver.Hash()[key]; ok { - return value, nil - } - if len(args) == 2 { - return args[1], nil - } - return NewNil(), nil - }), nil - case "dig": - return NewAutoBuiltin("hash.dig", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) == 0 { - return NewNil(), fmt.Errorf("hash.dig expects at least one key") - } - current := receiver - for _, arg := range args { - key, err := valueToHashKey(arg) - if err != nil { - return NewNil(), fmt.Errorf("hash.dig path keys must be symbol or string") - } - if current.Kind() != KindHash && current.Kind() != KindObject { - return NewNil(), nil - } - next, ok := current.Hash()[key] - if !ok { - return NewNil(), nil - } - current = next - } - return current, nil - }), nil - case "each": - return NewAutoBuiltin("hash.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.each does not take arguments") - } - if err := ensureBlock(block, "hash.each"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - for _, key := range sortedHashKeys(entries) { - if _, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "each_key": - return NewAutoBuiltin("hash.each_key", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.each_key does not take arguments") - } - if err := ensureBlock(block, "hash.each_key"); err != nil { - return NewNil(), err - } - for _, key := range sortedHashKeys(receiver.Hash()) { - if _, err := exec.CallBlock(block, []Value{NewSymbol(key)}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "each_value": - return NewAutoBuiltin("hash.each_value", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.each_value does not take arguments") - } - if err := ensureBlock(block, "hash.each_value"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - for _, key := range sortedHashKeys(entries) { - if _, err := exec.CallBlock(block, []Value{entries[key]}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "merge": - return NewBuiltin("hash.merge", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { - return NewNil(), fmt.Errorf("hash.merge expects a single hash argument") - } - base := receiver.Hash() - addition := args[0].Hash() - out := make(map[string]Value, len(base)+len(addition)) - maps.Copy(out, base) - maps.Copy(out, addition) - return NewHash(out), nil - }), nil - case "slice": - return NewBuiltin("hash.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - entries := receiver.Hash() - out := make(map[string]Value, len(args)) - for _, arg := range args { - key, err := valueToHashKey(arg) - if err != nil { - return NewNil(), fmt.Errorf("hash.slice keys must be symbol or string") - } - if value, ok := entries[key]; ok { - out[key] = value - } - } - return NewHash(out), nil - }), nil - case "except": - return NewBuiltin("hash.except", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - excluded := make(map[string]struct{}, len(args)) - for _, arg := range args { - key, err := valueToHashKey(arg) - if err != nil { - return NewNil(), fmt.Errorf("hash.except keys must be symbol or string") - } - excluded[key] = struct{}{} - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for key, value := range entries { - if _, skip := excluded[key]; skip { - continue - } - out[key] = value - } - return NewHash(out), nil - }), nil - case "select": - return NewAutoBuiltin("hash.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.select does not take arguments") - } - if err := ensureBlock(block, "hash.select"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - include, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) - if err != nil { - return NewNil(), err - } - if include.Truthy() { - out[key] = entries[key] - } - } - return NewHash(out), nil - }), nil - case "reject": - return NewAutoBuiltin("hash.reject", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.reject does not take arguments") - } - if err := ensureBlock(block, "hash.reject"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - exclude, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) - if err != nil { - return NewNil(), err - } - if !exclude.Truthy() { - out[key] = entries[key] - } - } - return NewHash(out), nil - }), nil - case "transform_keys": - return NewAutoBuiltin("hash.transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.transform_keys does not take arguments") - } - if err := ensureBlock(block, "hash.transform_keys"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - nextKey, err := exec.CallBlock(block, []Value{NewSymbol(key)}) - if err != nil { - return NewNil(), err - } - resolved, err := valueToHashKey(nextKey) - if err != nil { - return NewNil(), fmt.Errorf("hash.transform_keys block must return symbol or string") - } - out[resolved] = entries[key] - } - return NewHash(out), nil - }), nil - case "deep_transform_keys": - return NewAutoBuiltin("hash.deep_transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.deep_transform_keys does not take arguments") - } - if err := ensureBlock(block, "hash.deep_transform_keys"); err != nil { - return NewNil(), err - } - return deepTransformKeys(exec, receiver, block) - }), nil - case "remap_keys": - return NewBuiltin("hash.remap_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { - return NewNil(), fmt.Errorf("hash.remap_keys expects a key mapping hash") - } - entries := receiver.Hash() - mapping := args[0].Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - value := entries[key] - if mapped, ok := mapping[key]; ok { - nextKey, err := valueToHashKey(mapped) - if err != nil { - return NewNil(), fmt.Errorf("hash.remap_keys mapping values must be symbol or string") - } - out[nextKey] = value - continue - } - out[key] = value - } - return NewHash(out), nil - }), nil - case "transform_values": - return NewAutoBuiltin("hash.transform_values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.transform_values does not take arguments") - } - if err := ensureBlock(block, "hash.transform_values"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - nextValue, err := exec.CallBlock(block, []Value{entries[key]}) - if err != nil { - return NewNil(), err - } - out[key] = nextValue - } - return NewHash(out), nil - }), nil - case "compact": - return NewAutoBuiltin("hash.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.compact does not take arguments") - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for k, v := range entries { - if v.Kind() != KindNil { - out[k] = v - } - } - return NewHash(out), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown hash method %s", property) - } -} - -func sortedHashKeys(entries map[string]Value) []string { - keys := make([]string, 0, len(entries)) - for key := range entries { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -func deepTransformKeys(exec *Execution, value Value, block Value) (Value, error) { - return deepTransformKeysWithState(exec, value, block, &deepTransformState{ - seenHashes: make(map[uintptr]struct{}), - seenArrays: make(map[uintptr]struct{}), - }) -} - -type deepTransformState struct { - seenHashes map[uintptr]struct{} - seenArrays map[uintptr]struct{} -} - -func deepTransformKeysWithState(exec *Execution, value Value, block Value, state *deepTransformState) (Value, error) { - switch value.Kind() { - case KindHash, KindObject: - entries := value.Hash() - id := reflect.ValueOf(entries).Pointer() - if id != 0 { - if _, seen := state.seenHashes[id]; seen { - return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") - } - state.seenHashes[id] = struct{}{} - defer delete(state.seenHashes, id) - } - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - nextKeyValue, err := exec.CallBlock(block, []Value{NewSymbol(key)}) - if err != nil { - return NewNil(), err - } - nextKey, err := valueToHashKey(nextKeyValue) - if err != nil { - return NewNil(), fmt.Errorf("hash.deep_transform_keys block must return symbol or string") - } - nextValue, err := deepTransformKeysWithState(exec, entries[key], block, state) - if err != nil { - return NewNil(), err - } - out[nextKey] = nextValue - } - return NewHash(out), nil - case KindArray: - items := value.Array() - id := reflect.ValueOf(items).Pointer() - if id != 0 { - if _, seen := state.seenArrays[id]; seen { - return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") - } - state.seenArrays[id] = struct{}{} - defer delete(state.seenArrays, id) - } - out := make([]Value, len(items)) - for i, item := range items { - nextValue, err := deepTransformKeysWithState(exec, item, block, state) - if err != nil { - return NewNil(), err - } - out[i] = nextValue - } - return NewArray(out), nil - default: - return value, nil - } -} - -func chompDefault(text string) string { - if strings.HasSuffix(text, "\r\n") { - return text[:len(text)-2] - } - if strings.HasSuffix(text, "\n") || strings.HasSuffix(text, "\r") { - return text[:len(text)-1] - } - return text -} - -func stringRuneIndex(text, needle string, offset int) int { - hayRunes := []rune(text) - needleRunes := []rune(needle) - if offset < 0 || offset > len(hayRunes) { - return -1 - } - if len(needleRunes) == 0 { - return offset - } - limit := len(hayRunes) - len(needleRunes) - if limit < offset { - return -1 - } - for i := offset; i <= limit; i++ { - match := true - for j := range len(needleRunes) { - if hayRunes[i+j] != needleRunes[j] { - match = false - break - } - } - if match { - return i - } - } - return -1 -} - -func stringRuneRIndex(text, needle string, offset int) int { - hayRunes := []rune(text) - needleRunes := []rune(needle) - if offset < 0 { - return -1 - } - if offset > len(hayRunes) { - offset = len(hayRunes) - } - if len(needleRunes) == 0 { - return offset - } - if len(needleRunes) > len(hayRunes) { - return -1 - } - start := offset - maxStart := len(hayRunes) - len(needleRunes) - if start > maxStart { - start = maxStart - } - for i := start; i >= 0; i-- { - match := true - for j := range len(needleRunes) { - if hayRunes[i+j] != needleRunes[j] { - match = false - break - } - } - if match { - return i - } - } - return -1 -} - -func stringRuneSlice(text string, start, length int) (string, bool) { - runes := []rune(text) - if start < 0 || start >= len(runes) { - return "", false - } - if length < 0 { - return "", false - } - remaining := len(runes) - start - if length >= remaining { - return string(runes[start:]), true - } - end := start + length - return string(runes[start:end]), true -} - -func stringCapitalize(text string) string { - runes := []rune(text) - if len(runes) == 0 { - return "" - } - runes[0] = unicode.ToUpper(runes[0]) - for i := 1; i < len(runes); i++ { - runes[i] = unicode.ToLower(runes[i]) - } - return string(runes) -} - -func stringSwapCase(text string) string { - runes := []rune(text) - for i, r := range runes { - if unicode.IsUpper(r) { - runes[i] = unicode.ToLower(r) - continue - } - if unicode.IsLower(r) { - runes[i] = unicode.ToUpper(r) - } - } - return string(runes) -} - -func stringReverse(text string) string { - runes := []rune(text) - for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { - runes[i], runes[j] = runes[j], runes[i] - } - return string(runes) -} - -func stringRegexOption(method string, kwargs map[string]Value) (bool, error) { - if len(kwargs) == 0 { - return false, nil - } - regexVal, ok := kwargs["regex"] - if !ok || len(kwargs) > 1 { - return false, fmt.Errorf("string.%s supports only regex keyword", method) - } - if regexVal.Kind() != KindBool { - return false, fmt.Errorf("string.%s regex keyword must be bool", method) - } - return regexVal.Bool(), nil -} - -func stringSub(text, pattern, replacement string, regex bool) (string, error) { - if !regex { - return strings.Replace(text, pattern, replacement, 1), nil - } - re, err := regexp.Compile(pattern) - if err != nil { - return "", err - } - loc := re.FindStringSubmatchIndex(text) - if loc == nil { - return text, nil - } - replaced := re.ExpandString(nil, replacement, text, loc) - return text[:loc[0]] + string(replaced) + text[loc[1]:], nil -} - -func stringGSub(text, pattern, replacement string, regex bool) (string, error) { - if !regex { - return strings.ReplaceAll(text, pattern, replacement), nil - } - re, err := regexp.Compile(pattern) - if err != nil { - return "", err - } - return re.ReplaceAllString(text, replacement), nil -} - -func stringBangResult(original, updated string) Value { - if updated == original { - return NewNil() - } - return NewString(updated) -} - -func stringSquish(text string) string { - return strings.Join(strings.Fields(text), " ") -} - -func stringTemplateOption(kwargs map[string]Value) (bool, error) { - if len(kwargs) == 0 { - return false, nil - } - value, ok := kwargs["strict"] - if !ok || len(kwargs) != 1 { - return false, fmt.Errorf("string.template supports only strict keyword") - } - if value.Kind() != KindBool { - return false, fmt.Errorf("string.template strict keyword must be bool") - } - return value.Bool(), nil -} - -func stringTemplateLookup(context Value, keyPath string) (Value, bool) { - current := context - for _, segment := range strings.Split(keyPath, ".") { - if segment == "" { - return NewNil(), false - } - if current.Kind() != KindHash && current.Kind() != KindObject { - return NewNil(), false - } - next, ok := current.Hash()[segment] - if !ok { - return NewNil(), false - } - current = next - } - return current, true -} - -func stringTemplateScalarValue(value Value, keyPath string) (string, error) { - switch value.Kind() { - case KindNil, KindBool, KindInt, KindFloat, KindString, KindSymbol, KindMoney, KindDuration, KindTime: - return value.String(), nil - default: - return "", fmt.Errorf("string.template placeholder %s value must be scalar", keyPath) - } -} - -func stringTemplate(text string, context Value, strict bool) (string, error) { - templateErr := error(nil) - rendered := stringTemplatePattern.ReplaceAllStringFunc(text, func(match string) string { - if templateErr != nil { - return match - } - submatch := stringTemplatePattern.FindStringSubmatch(match) - if len(submatch) != 2 { - return match - } - keyPath := submatch[1] - value, ok := stringTemplateLookup(context, keyPath) - if !ok { - if strict { - templateErr = fmt.Errorf("string.template missing placeholder %s", keyPath) - } - return match - } - segment, err := stringTemplateScalarValue(value, keyPath) - if err != nil { - templateErr = err - return match - } - return segment - }) - if templateErr != nil { - return "", templateErr - } - return rendered, nil -} - -func stringMember(str Value, property string) (Value, error) { - switch property { - case "size": - return NewAutoBuiltin("string.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.size does not take arguments") - } - return NewInt(int64(len([]rune(receiver.String())))), nil - }), nil - case "length": - return NewAutoBuiltin("string.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.length does not take arguments") - } - return NewInt(int64(len([]rune(receiver.String())))), nil - }), nil - case "bytesize": - return NewAutoBuiltin("string.bytesize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.bytesize does not take arguments") - } - return NewInt(int64(len(receiver.String()))), nil - }), nil - case "ord": - return NewAutoBuiltin("string.ord", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.ord does not take arguments") - } - runes := []rune(receiver.String()) - if len(runes) == 0 { - return NewNil(), fmt.Errorf("string.ord requires non-empty string") - } - return NewInt(int64(runes[0])), nil - }), nil - case "chr": - return NewAutoBuiltin("string.chr", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.chr does not take arguments") - } - runes := []rune(receiver.String()) - if len(runes) == 0 { - return NewNil(), nil - } - return NewString(string(runes[0])), nil - }), nil - case "empty?": - return NewAutoBuiltin("string.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.empty? does not take arguments") - } - return NewBool(len(receiver.String()) == 0), nil - }), nil - case "clear": - return NewAutoBuiltin("string.clear", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.clear does not take arguments") - } - return NewString(""), nil - }), nil - case "concat": - return NewAutoBuiltin("string.concat", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - var b strings.Builder - b.WriteString(receiver.String()) - for _, arg := range args { - if arg.Kind() != KindString { - return NewNil(), fmt.Errorf("string.concat expects string arguments") - } - b.WriteString(arg.String()) - } - return NewString(b.String()), nil - }), nil - case "replace": - return NewAutoBuiltin("string.replace", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.replace expects exactly one replacement") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.replace replacement must be string") - } - return NewString(args[0].String()), nil - }), nil - case "start_with?": - return NewAutoBuiltin("string.start_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.start_with? expects exactly one prefix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.start_with? prefix must be string") - } - return NewBool(strings.HasPrefix(receiver.String(), args[0].String())), nil - }), nil - case "end_with?": - return NewAutoBuiltin("string.end_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.end_with? expects exactly one suffix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.end_with? suffix must be string") - } - return NewBool(strings.HasSuffix(receiver.String(), args[0].String())), nil - }), nil - case "include?": - return NewAutoBuiltin("string.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.include? expects exactly one substring") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.include? substring must be string") - } - return NewBool(strings.Contains(receiver.String(), args[0].String())), nil - }), nil - case "match": - return NewAutoBuiltin("string.match", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("string.match does not take keyword arguments") - } - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.match expects exactly one pattern") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.match pattern must be string") - } - pattern := args[0].String() - re, err := regexp.Compile(pattern) - if err != nil { - return NewNil(), fmt.Errorf("string.match invalid regex: %v", err) - } - text := receiver.String() - indices := re.FindStringSubmatchIndex(text) - if indices == nil { - return NewNil(), nil - } - values := make([]Value, len(indices)/2) - for i := range values { - start := indices[i*2] - end := indices[i*2+1] - if start < 0 || end < 0 { - values[i] = NewNil() - continue - } - values[i] = NewString(text[start:end]) - } - return NewArray(values), nil - }), nil - case "scan": - return NewAutoBuiltin("string.scan", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("string.scan does not take keyword arguments") - } - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.scan expects exactly one pattern") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.scan pattern must be string") - } - pattern := args[0].String() - re, err := regexp.Compile(pattern) - if err != nil { - return NewNil(), fmt.Errorf("string.scan invalid regex: %v", err) - } - matches := re.FindAllString(receiver.String(), -1) - values := make([]Value, len(matches)) - for i, m := range matches { - values[i] = NewString(m) - } - return NewArray(values), nil - }), nil - case "index": - return NewAutoBuiltin("string.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("string.index expects substring and optional offset") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.index substring must be string") - } - offset := 0 - if len(args) == 2 { - i, err := valueToInt(args[1]) - if err != nil || i < 0 { - return NewNil(), fmt.Errorf("string.index offset must be non-negative integer") - } - offset = i - } - index := stringRuneIndex(receiver.String(), args[0].String(), offset) - if index < 0 { - return NewNil(), nil - } - return NewInt(int64(index)), nil - }), nil - case "rindex": - return NewAutoBuiltin("string.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("string.rindex expects substring and optional offset") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.rindex substring must be string") - } - offset := len([]rune(receiver.String())) - if len(args) == 2 { - i, err := valueToInt(args[1]) - if err != nil || i < 0 { - return NewNil(), fmt.Errorf("string.rindex offset must be non-negative integer") - } - offset = i - } - index := stringRuneRIndex(receiver.String(), args[0].String(), offset) - if index < 0 { - return NewNil(), nil - } - return NewInt(int64(index)), nil - }), nil - case "slice": - return NewAutoBuiltin("string.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("string.slice expects index and optional length") - } - start, err := valueToInt(args[0]) - if err != nil { - return NewNil(), fmt.Errorf("string.slice index must be integer") - } - runes := []rune(receiver.String()) - if len(args) == 1 { - if start < 0 || start >= len(runes) { - return NewNil(), nil - } - return NewString(string(runes[start])), nil - } - length, err := valueToInt(args[1]) - if err != nil { - return NewNil(), fmt.Errorf("string.slice length must be integer") - } - substr, ok := stringRuneSlice(receiver.String(), start, length) - if !ok { - return NewNil(), nil - } - return NewString(substr), nil - }), nil - case "strip": - return NewAutoBuiltin("string.strip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.strip does not take arguments") - } - return NewString(strings.TrimSpace(receiver.String())), nil - }), nil - case "strip!": - return NewAutoBuiltin("string.strip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.strip! does not take arguments") - } - updated := strings.TrimSpace(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "squish": - return NewAutoBuiltin("string.squish", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.squish does not take arguments") - } - return NewString(stringSquish(receiver.String())), nil - }), nil - case "squish!": - return NewAutoBuiltin("string.squish!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.squish! does not take arguments") - } - updated := stringSquish(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "lstrip": - return NewAutoBuiltin("string.lstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.lstrip does not take arguments") - } - return NewString(strings.TrimLeftFunc(receiver.String(), unicode.IsSpace)), nil - }), nil - case "lstrip!": - return NewAutoBuiltin("string.lstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.lstrip! does not take arguments") - } - updated := strings.TrimLeftFunc(receiver.String(), unicode.IsSpace) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "rstrip": - return NewAutoBuiltin("string.rstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.rstrip does not take arguments") - } - return NewString(strings.TrimRightFunc(receiver.String(), unicode.IsSpace)), nil - }), nil - case "rstrip!": - return NewAutoBuiltin("string.rstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.rstrip! does not take arguments") - } - updated := strings.TrimRightFunc(receiver.String(), unicode.IsSpace) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "chomp": - return NewAutoBuiltin("string.chomp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("string.chomp accepts at most one separator") - } - text := receiver.String() - if len(args) == 0 { - return NewString(chompDefault(text)), nil - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.chomp separator must be string") - } - sep := args[0].String() - if sep == "" { - return NewString(strings.TrimRight(text, "\r\n")), nil - } - if strings.HasSuffix(text, sep) { - return NewString(text[:len(text)-len(sep)]), nil - } - return NewString(text), nil - }), nil - case "chomp!": - return NewAutoBuiltin("string.chomp!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("string.chomp! accepts at most one separator") - } - original := receiver.String() - if len(args) == 0 { - return stringBangResult(original, chompDefault(original)), nil - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.chomp! separator must be string") - } - sep := args[0].String() - if sep == "" { - return stringBangResult(original, strings.TrimRight(original, "\r\n")), nil - } - if strings.HasSuffix(original, sep) { - return stringBangResult(original, original[:len(original)-len(sep)]), nil - } - return NewNil(), nil - }), nil - case "delete_prefix": - return NewAutoBuiltin("string.delete_prefix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_prefix expects exactly one prefix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_prefix prefix must be string") - } - return NewString(strings.TrimPrefix(receiver.String(), args[0].String())), nil - }), nil - case "delete_prefix!": - return NewAutoBuiltin("string.delete_prefix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_prefix! expects exactly one prefix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_prefix! prefix must be string") - } - updated := strings.TrimPrefix(receiver.String(), args[0].String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "delete_suffix": - return NewAutoBuiltin("string.delete_suffix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_suffix expects exactly one suffix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_suffix suffix must be string") - } - return NewString(strings.TrimSuffix(receiver.String(), args[0].String())), nil - }), nil - case "delete_suffix!": - return NewAutoBuiltin("string.delete_suffix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_suffix! expects exactly one suffix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_suffix! suffix must be string") - } - updated := strings.TrimSuffix(receiver.String(), args[0].String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "upcase": - return NewAutoBuiltin("string.upcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.upcase does not take arguments") - } - return NewString(strings.ToUpper(receiver.String())), nil - }), nil - case "upcase!": - return NewAutoBuiltin("string.upcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.upcase! does not take arguments") - } - updated := strings.ToUpper(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "downcase": - return NewAutoBuiltin("string.downcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.downcase does not take arguments") - } - return NewString(strings.ToLower(receiver.String())), nil - }), nil - case "downcase!": - return NewAutoBuiltin("string.downcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.downcase! does not take arguments") - } - updated := strings.ToLower(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "capitalize": - return NewAutoBuiltin("string.capitalize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.capitalize does not take arguments") - } - return NewString(stringCapitalize(receiver.String())), nil - }), nil - case "capitalize!": - return NewAutoBuiltin("string.capitalize!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.capitalize! does not take arguments") - } - updated := stringCapitalize(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "swapcase": - return NewAutoBuiltin("string.swapcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.swapcase does not take arguments") - } - return NewString(stringSwapCase(receiver.String())), nil - }), nil - case "swapcase!": - return NewAutoBuiltin("string.swapcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.swapcase! does not take arguments") - } - updated := stringSwapCase(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "reverse": - return NewAutoBuiltin("string.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.reverse does not take arguments") - } - return NewString(stringReverse(receiver.String())), nil - }), nil - case "reverse!": - return NewAutoBuiltin("string.reverse!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.reverse! does not take arguments") - } - updated := stringReverse(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "sub": - return NewAutoBuiltin("string.sub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.sub expects pattern and replacement") - } - regex, err := stringRegexOption("sub", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub replacement must be string") - } - updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.sub invalid regex: %v", err) - } - return NewString(updated), nil - }), nil - case "sub!": - return NewAutoBuiltin("string.sub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.sub! expects pattern and replacement") - } - regex, err := stringRegexOption("sub!", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub! pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub! replacement must be string") - } - updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.sub! invalid regex: %v", err) - } - return stringBangResult(receiver.String(), updated), nil - }), nil - case "gsub": - return NewAutoBuiltin("string.gsub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.gsub expects pattern and replacement") - } - regex, err := stringRegexOption("gsub", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub replacement must be string") - } - updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.gsub invalid regex: %v", err) - } - return NewString(updated), nil - }), nil - case "gsub!": - return NewAutoBuiltin("string.gsub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.gsub! expects pattern and replacement") - } - regex, err := stringRegexOption("gsub!", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub! pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub! replacement must be string") - } - updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.gsub! invalid regex: %v", err) - } - return stringBangResult(receiver.String(), updated), nil - }), nil - case "split": - return NewAutoBuiltin("string.split", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("string.split accepts at most one separator") - } - text := receiver.String() - var parts []string - if len(args) == 0 { - parts = strings.Fields(text) - } else { - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.split separator must be string") - } - parts = strings.Split(text, args[0].String()) - } - values := make([]Value, len(parts)) - for i, part := range parts { - values[i] = NewString(part) - } - return NewArray(values), nil - }), nil - case "template": - return NewAutoBuiltin("string.template", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.template expects exactly one context hash") - } - if args[0].Kind() != KindHash && args[0].Kind() != KindObject { - return NewNil(), fmt.Errorf("string.template context must be hash") - } - strict, err := stringTemplateOption(kwargs) - if err != nil { - return NewNil(), err - } - rendered, err := stringTemplate(receiver.String(), args[0], strict) - if err != nil { - return NewNil(), err - } - return NewString(rendered), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown string method %s", property) - } -} - -func durationMember(d Duration, property string, pos Position) (Value, error) { - switch property { - case "seconds", "second": - return NewInt(d.Seconds()), nil - case "minutes", "minute": - return NewInt(d.Seconds() / 60), nil - case "hours", "hour": - return NewInt(d.Seconds() / 3600), nil - case "days", "day": - return NewInt(d.Seconds() / 86400), nil - case "weeks", "week": - return NewInt(d.Seconds() / 604800), nil - case "in_seconds": - return NewFloat(float64(d.Seconds())), nil - case "in_minutes": - return NewFloat(float64(d.Seconds()) / 60), nil - case "in_hours": - return NewFloat(float64(d.Seconds()) / 3600), nil - case "in_days": - return NewFloat(float64(d.Seconds()) / 86400), nil - case "in_weeks": - return NewFloat(float64(d.Seconds()) / 604800), nil - case "in_months": - return NewFloat(float64(d.Seconds()) / (30 * 86400)), nil - case "in_years": - return NewFloat(float64(d.Seconds()) / (365 * 86400)), nil - case "iso8601": - return NewString(d.iso8601()), nil - case "parts": - p := d.parts() - return NewHash(map[string]Value{ - "days": NewInt(p["days"]), - "hours": NewInt(p["hours"]), - "minutes": NewInt(p["minutes"]), - "seconds": NewInt(p["seconds"]), - }), nil - case "to_i": - return NewInt(d.Seconds()), nil - case "to_s": - return NewString(d.String()), nil - case "format": - return NewString(d.String()), nil - case "eql?": - return NewBuiltin("duration.eql?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 || args[0].Kind() != KindDuration { - return NewNil(), fmt.Errorf("duration.eql? expects a duration") - } - return NewBool(d.Seconds() == args[0].Duration().Seconds()), nil - }), nil - case "after", "since", "from_now": - return NewBuiltin("duration.after", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - start, err := durationTimeArg(args, true, "after") - if err != nil { - return NewNil(), err - } - result := start.Add(time.Duration(d.Seconds()) * time.Second).UTC() - return NewTime(result), nil - }), nil - case "ago", "before", "until": - return NewBuiltin("duration.before", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - start, err := durationTimeArg(args, true, "before") - if err != nil { - return NewNil(), err - } - result := start.Add(-time.Duration(d.Seconds()) * time.Second).UTC() - return NewTime(result), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown duration method %s", property) - } -} - -func durationTimeArg(args []Value, allowEmpty bool, name string) (time.Time, error) { - if len(args) == 0 { - if allowEmpty { - return time.Now().UTC(), nil - } - return time.Time{}, fmt.Errorf("%s expects a time argument", name) - } - if len(args) != 1 { - return time.Time{}, fmt.Errorf("%s expects at most one time argument", name) - } - val := args[0] - switch val.Kind() { - case KindString: - t, err := time.Parse(time.RFC3339, val.String()) - if err != nil { - return time.Time{}, fmt.Errorf("invalid time: %v", err) - } - return t.UTC(), nil - case KindTime: - return val.Time(), nil - default: - return time.Time{}, fmt.Errorf("%s expects a Time or RFC3339 string", name) - } -} - -func arrayMember(array Value, property string) (Value, error) { - switch property { - case "size": - return NewAutoBuiltin("array.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.size does not take arguments") - } - return NewInt(int64(len(receiver.Array()))), nil - }), nil - case "each": - return NewAutoBuiltin("array.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.each"); err != nil { - return NewNil(), err - } - for _, item := range receiver.Array() { - if _, err := exec.CallBlock(block, []Value{item}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "map": - return NewAutoBuiltin("array.map", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.map"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - result := make([]Value, len(arr)) - for i, item := range arr { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - result[i] = val - } - return NewArray(result), nil - }), nil - case "select": - return NewAutoBuiltin("array.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.select"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - out := make([]Value, 0, len(arr)) - for _, item := range arr { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if val.Truthy() { - out = append(out, item) - } - } - return NewArray(out), nil - }), nil - case "find": - return NewAutoBuiltin("array.find", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.find does not take arguments") - } - if err := ensureBlock(block, "array.find"); err != nil { - return NewNil(), err - } - for _, item := range receiver.Array() { - match, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if match.Truthy() { - return item, nil - } - } - return NewNil(), nil - }), nil - case "find_index": - return NewAutoBuiltin("array.find_index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.find_index does not take arguments") - } - if err := ensureBlock(block, "array.find_index"); err != nil { - return NewNil(), err - } - for idx, item := range receiver.Array() { - match, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if match.Truthy() { - return NewInt(int64(idx)), nil - } - } - return NewNil(), nil - }), nil - case "reduce": - return NewAutoBuiltin("array.reduce", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.reduce"); err != nil { - return NewNil(), err - } - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.reduce accepts at most one initial value") - } - arr := receiver.Array() - if len(arr) == 0 && len(args) == 0 { - return NewNil(), fmt.Errorf("array.reduce on empty array requires an initial value") - } - var acc Value - start := 0 - if len(args) == 1 { - acc = args[0] - } else { - acc = arr[0] - start = 1 - } - for i := start; i < len(arr); i++ { - next, err := exec.CallBlock(block, []Value{acc, arr[i]}) - if err != nil { - return NewNil(), err - } - acc = next - } - return acc, nil - }), nil - case "include?": - return NewAutoBuiltin("array.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("array.include? expects exactly one value") - } - for _, item := range receiver.Array() { - if item.Equal(args[0]) { - return NewBool(true), nil - } - } - return NewBool(false), nil - }), nil - case "index": - return NewAutoBuiltin("array.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("array.index expects value and optional offset") - } - offset := 0 - if len(args) == 2 { - n, err := valueToInt(args[1]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.index offset must be non-negative integer") - } - offset = n - } - arr := receiver.Array() - if offset >= len(arr) { - return NewNil(), nil - } - for idx := offset; idx < len(arr); idx++ { - if arr[idx].Equal(args[0]) { - return NewInt(int64(idx)), nil - } - } - return NewNil(), nil - }), nil - case "rindex": - return NewAutoBuiltin("array.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("array.rindex expects value and optional offset") - } - offset := -1 - if len(args) == 2 { - n, err := valueToInt(args[1]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.rindex offset must be non-negative integer") - } - offset = n - } - arr := receiver.Array() - if len(arr) == 0 { - return NewNil(), nil - } - if offset < 0 { - offset = len(arr) - 1 - } - if offset >= len(arr) { - offset = len(arr) - 1 - } - for idx := offset; idx >= 0; idx-- { - if arr[idx].Equal(args[0]) { - return NewInt(int64(idx)), nil - } - } - return NewNil(), nil - }), nil - case "count": - return NewAutoBuiltin("array.count", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.count accepts at most one value argument") - } - arr := receiver.Array() - if len(args) == 1 { - if block.Block() != nil { - return NewNil(), fmt.Errorf("array.count does not accept both argument and block") - } - total := int64(0) - for _, item := range arr { - if item.Equal(args[0]) { - total++ - } - } - return NewInt(total), nil - } - if block.Block() == nil { - return NewInt(int64(len(arr))), nil - } - total := int64(0) - for _, item := range arr { - include, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if include.Truthy() { - total++ - } - } - return NewInt(total), nil - }), nil - case "any?": - return NewAutoBuiltin("array.any?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.any? does not take arguments") - } - for _, item := range receiver.Array() { - if block.Block() != nil { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if val.Truthy() { - return NewBool(true), nil - } - continue - } - if item.Truthy() { - return NewBool(true), nil - } - } - return NewBool(false), nil - }), nil - case "all?": - return NewAutoBuiltin("array.all?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.all? does not take arguments") - } - for _, item := range receiver.Array() { - if block.Block() != nil { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if !val.Truthy() { - return NewBool(false), nil - } - continue - } - if !item.Truthy() { - return NewBool(false), nil - } - } - return NewBool(true), nil - }), nil - case "none?": - return NewAutoBuiltin("array.none?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.none? does not take arguments") - } - for _, item := range receiver.Array() { - if block.Block() != nil { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if val.Truthy() { - return NewBool(false), nil - } - continue - } - if item.Truthy() { - return NewBool(false), nil - } - } - return NewBool(true), nil - }), nil - case "push": - return NewBuiltin("array.push", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) == 0 { - return NewNil(), fmt.Errorf("array.push expects at least one argument") - } - base := receiver.Array() - out := make([]Value, len(base)+len(args)) - copy(out, base) - copy(out[len(base):], args) - return NewArray(out), nil - }), nil - case "pop": - return NewAutoBuiltin("array.pop", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.pop accepts at most one argument") - } - count := 1 - if len(args) == 1 { - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.pop expects non-negative integer") - } - count = n - } - arr := receiver.Array() - if count == 0 { - return NewHash(map[string]Value{ - "array": NewArray(arr), - "popped": NewNil(), - }), nil - } - if len(arr) == 0 { - popped := NewNil() - if len(args) == 1 { - popped = NewArray([]Value{}) - } - return NewHash(map[string]Value{ - "array": NewArray([]Value{}), - "popped": popped, - }), nil - } - if count > len(arr) { - count = len(arr) - } - remaining := make([]Value, len(arr)-count) - copy(remaining, arr[:len(arr)-count]) - removed := make([]Value, count) - copy(removed, arr[len(arr)-count:]) - result := map[string]Value{ - "array": NewArray(remaining), - } - if count == 1 && len(args) == 0 { - result["popped"] = removed[0] - } else { - result["popped"] = NewArray(removed) - } - return NewHash(result), nil - }), nil - case "uniq": - return NewAutoBuiltin("array.uniq", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.uniq does not take arguments") - } - arr := receiver.Array() - unique := make([]Value, 0, len(arr)) - for _, item := range arr { - found := slices.ContainsFunc(unique, item.Equal) - if !found { - unique = append(unique, item) - } - } - return NewArray(unique), nil - }), nil - case "first": - return NewAutoBuiltin("array.first", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - arr := receiver.Array() - if len(args) == 0 { - if len(arr) == 0 { - return NewNil(), nil - } - return arr[0], nil - } - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.first expects non-negative integer") - } - if n > len(arr) { - n = len(arr) - } - out := make([]Value, n) - copy(out, arr[:n]) - return NewArray(out), nil - }), nil - case "last": - return NewAutoBuiltin("array.last", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - arr := receiver.Array() - if len(args) == 0 { - if len(arr) == 0 { - return NewNil(), nil - } - return arr[len(arr)-1], nil - } - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.last expects non-negative integer") - } - if n > len(arr) { - n = len(arr) - } - out := make([]Value, n) - copy(out, arr[len(arr)-n:]) - return NewArray(out), nil - }), nil - case "sum": - return NewAutoBuiltin("array.sum", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - arr := receiver.Array() - total := NewInt(0) - for _, item := range arr { - switch item.Kind() { - case KindInt, KindFloat: - default: - return NewNil(), fmt.Errorf("array.sum supports numeric values") - } - sum, err := addValues(total, item) - if err != nil { - return NewNil(), fmt.Errorf("array.sum supports numeric values") - } - total = sum - } - return total, nil - }), nil - case "compact": - return NewAutoBuiltin("array.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.compact does not take arguments") - } - arr := receiver.Array() - out := make([]Value, 0, len(arr)) - for _, item := range arr { - if item.Kind() != KindNil { - out = append(out, item) - } - } - return NewArray(out), nil - }), nil - case "flatten": - return NewAutoBuiltin("array.flatten", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - // depth=-1 is a sentinel value meaning "flatten fully" (no depth limit) - depth := -1 - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.flatten accepts at most one depth argument") - } - if len(args) == 1 { - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.flatten depth must be non-negative integer") - } - depth = n - } - arr := receiver.Array() - out := flattenValues(arr, depth) - return NewArray(out), nil - }), nil - case "chunk": - return NewAutoBuiltin("array.chunk", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("array.chunk expects a chunk size") - } - sizeValue := args[0] - maxNativeInt := int64(^uint(0) >> 1) - if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { - return NewNil(), fmt.Errorf("array.chunk size must be a positive integer") - } - size := int(sizeValue.Int()) - arr := receiver.Array() - if len(arr) == 0 { - return NewArray([]Value{}), nil - } - chunkCapacity := len(arr) / size - if len(arr)%size != 0 { - chunkCapacity++ - } - chunks := make([]Value, 0, chunkCapacity) - for i := 0; i < len(arr); i += size { - end := i + size - if end > len(arr) { - end = len(arr) - } - part := make([]Value, end-i) - copy(part, arr[i:end]) - chunks = append(chunks, NewArray(part)) - } - return NewArray(chunks), nil - }), nil - case "window": - return NewAutoBuiltin("array.window", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("array.window expects a window size") - } - sizeValue := args[0] - maxNativeInt := int64(^uint(0) >> 1) - if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { - return NewNil(), fmt.Errorf("array.window size must be a positive integer") - } - size := int(sizeValue.Int()) - arr := receiver.Array() - if size > len(arr) { - return NewArray([]Value{}), nil - } - windows := make([]Value, 0, len(arr)-size+1) - for i := 0; i+size <= len(arr); i++ { - part := make([]Value, size) - copy(part, arr[i:i+size]) - windows = append(windows, NewArray(part)) - } - return NewArray(windows), nil - }), nil - case "join": - return NewAutoBuiltin("array.join", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.join accepts at most one separator") - } - sep := "" - if len(args) == 1 { - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("array.join separator must be string") - } - sep = args[0].String() - } - arr := receiver.Array() - if len(arr) == 0 { - return NewString(""), nil - } - // Use strings.Builder for efficient concatenation - var b strings.Builder - for i, item := range arr { - if i > 0 { - b.WriteString(sep) - } - b.WriteString(item.String()) - } - return NewString(b.String()), nil - }), nil - case "reverse": - return NewAutoBuiltin("array.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.reverse does not take arguments") - } - arr := receiver.Array() - out := make([]Value, len(arr)) - for i, item := range arr { - out[len(arr)-1-i] = item - } - return NewArray(out), nil - }), nil - case "sort": - return NewAutoBuiltin("array.sort", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.sort does not take arguments") - } - arr := receiver.Array() - out := make([]Value, len(arr)) - copy(out, arr) - var sortErr error - sort.SliceStable(out, func(i, j int) bool { - if sortErr != nil { - return false - } - if block.Block() != nil { - cmpValue, err := exec.CallBlock(block, []Value{out[i], out[j]}) - if err != nil { - sortErr = err - return false - } - cmp, err := sortComparisonResult(cmpValue) - if err != nil { - sortErr = fmt.Errorf("array.sort block must return numeric comparator") - return false - } - return cmp < 0 - } - cmp, err := arraySortCompareValues(out[i], out[j]) - if err != nil { - sortErr = fmt.Errorf("array.sort values are not comparable") - return false - } - return cmp < 0 - }) - if sortErr != nil { - return NewNil(), sortErr - } - return NewArray(out), nil - }), nil - case "sort_by": - return NewAutoBuiltin("array.sort_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.sort_by does not take arguments") - } - if err := ensureBlock(block, "array.sort_by"); err != nil { - return NewNil(), err - } - type itemWithSortKey struct { - item Value - key Value - index int - } - arr := receiver.Array() - withKeys := make([]itemWithSortKey, len(arr)) - for i, item := range arr { - sortKey, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - withKeys[i] = itemWithSortKey{item: item, key: sortKey, index: i} - } - var sortErr error - sort.SliceStable(withKeys, func(i, j int) bool { - if sortErr != nil { - return false - } - cmp, err := arraySortCompareValues(withKeys[i].key, withKeys[j].key) - if err != nil { - sortErr = fmt.Errorf("array.sort_by block values are not comparable") - return false - } - if cmp == 0 { - return withKeys[i].index < withKeys[j].index - } - return cmp < 0 - }) - if sortErr != nil { - return NewNil(), sortErr - } - out := make([]Value, len(withKeys)) - for i, item := range withKeys { - out[i] = item.item - } - return NewArray(out), nil - }), nil - case "partition": - return NewAutoBuiltin("array.partition", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.partition does not take arguments") - } - if err := ensureBlock(block, "array.partition"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - left := make([]Value, 0, len(arr)) - right := make([]Value, 0, len(arr)) - for _, item := range arr { - match, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if match.Truthy() { - left = append(left, item) - } else { - right = append(right, item) - } - } - return NewArray([]Value{NewArray(left), NewArray(right)}), nil - }), nil - case "group_by": - return NewAutoBuiltin("array.group_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.group_by does not take arguments") - } - if err := ensureBlock(block, "array.group_by"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - groups := make(map[string][]Value, len(arr)) - for _, item := range arr { - groupValue, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - key, err := valueToHashKey(groupValue) - if err != nil { - return NewNil(), fmt.Errorf("array.group_by block must return symbol or string") - } - groups[key] = append(groups[key], item) - } - result := make(map[string]Value, len(groups)) - for key, items := range groups { - result[key] = NewArray(items) - } - return NewHash(result), nil - }), nil - case "group_by_stable": - return NewAutoBuiltin("array.group_by_stable", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.group_by_stable does not take arguments") - } - if err := ensureBlock(block, "array.group_by_stable"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - order := make([]string, 0, len(arr)) - keyValues := make(map[string]Value, len(arr)) - groups := make(map[string][]Value, len(arr)) - for _, item := range arr { - groupValue, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - key, err := valueToHashKey(groupValue) - if err != nil { - return NewNil(), fmt.Errorf("array.group_by_stable block must return symbol or string") - } - if _, exists := groups[key]; !exists { - order = append(order, key) - keyValues[key] = groupValue - groups[key] = []Value{} - } - groups[key] = append(groups[key], item) - } - result := make([]Value, 0, len(order)) - for _, key := range order { - result = append(result, NewArray([]Value{ - keyValues[key], - NewArray(groups[key]), - })) - } - return NewArray(result), nil - }), nil - case "tally": - return NewAutoBuiltin("array.tally", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.tally does not take arguments") - } - arr := receiver.Array() - counts := make(map[string]int64, len(arr)) - for _, item := range arr { - keyValue := item - if block.Block() != nil { - mapped, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - keyValue = mapped - } - key, err := valueToHashKey(keyValue) - if err != nil { - return NewNil(), fmt.Errorf("array.tally values must be symbol or string") - } - counts[key]++ - } - result := make(map[string]Value, len(counts)) - for key, count := range counts { - result[key] = NewInt(count) - } - return NewHash(result), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown array method %s", property) - } -} diff --git a/vibes/execution_members.go b/vibes/execution_members.go new file mode 100644 index 0000000..a2b255c --- /dev/null +++ b/vibes/execution_members.go @@ -0,0 +1,2329 @@ +package vibes + +import ( + "fmt" + "maps" + "math" + "reflect" + "regexp" + "slices" + "sort" + "strings" + "time" + "unicode" +) + +func (exec *Execution) getMember(obj Value, property string, pos Position) (Value, error) { + switch obj.Kind() { + case KindHash, KindObject: + if val, ok := obj.Hash()[property]; ok { + return val, nil + } + member, err := hashMember(obj, property) + if err != nil { + return NewNil(), err + } + return member, nil + case KindMoney: + return moneyMember(obj.Money(), property) + case KindDuration: + return durationMember(obj.Duration(), property, pos) + case KindTime: + return timeMember(obj.Time(), property) + case KindArray: + return arrayMember(obj, property) + case KindString: + return stringMember(obj, property) + case KindClass: + cl := obj.Class() + if property == "new" { + return NewAutoBuiltin(cl.Name+".new", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + inst := &Instance{Class: cl, Ivars: make(map[string]Value)} + instVal := NewInstance(inst) + if initFn, ok := cl.Methods["initialize"]; ok { + if _, err := exec.callFunction(initFn, instVal, args, kwargs, block, pos); err != nil { + return NewNil(), err + } + } + return instVal, nil + }), nil + } + if fn, ok := cl.ClassMethods[property]; ok { + if fn.Private && !exec.isCurrentReceiver(obj) { + return NewNil(), exec.errorAt(pos, "private method %s", property) + } + return NewAutoBuiltin(cl.Name+"."+property, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + return exec.callFunction(fn, obj, args, kwargs, block, pos) + }), nil + } + if val, ok := cl.ClassVars[property]; ok { + return val, nil + } + return NewNil(), exec.errorAt(pos, "unknown class member %s", property) + case KindInstance: + inst := obj.Instance() + if property == "class" { + return NewClass(inst.Class), nil + } + if fn, ok := inst.Class.Methods[property]; ok { + if fn.Private && !exec.isCurrentReceiver(obj) { + return NewNil(), exec.errorAt(pos, "private method %s", property) + } + return NewAutoBuiltin(inst.Class.Name+"#"+property, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + return exec.callFunction(fn, obj, args, kwargs, block, pos) + }), nil + } + if val, ok := inst.Ivars[property]; ok { + return val, nil + } + return NewNil(), exec.errorAt(pos, "unknown member %s", property) + case KindInt: + switch property { + case "seconds", "second", "minutes", "minute", "hours", "hour", "days", "day": + return NewDuration(secondsDuration(obj.Int(), property)), nil + case "weeks", "week": + return NewDuration(secondsDuration(obj.Int(), property)), nil + case "abs": + return NewAutoBuiltin("int.abs", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("int.abs does not take arguments") + } + n := receiver.Int() + if n == math.MinInt64 { + return NewNil(), fmt.Errorf("int.abs overflow") + } + if n < 0 { + return NewInt(-n), nil + } + return receiver, nil + }), nil + case "clamp": + return NewAutoBuiltin("int.clamp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("int.clamp expects min and max") + } + if args[0].Kind() != KindInt || args[1].Kind() != KindInt { + return NewNil(), fmt.Errorf("int.clamp expects integer min and max") + } + minVal := args[0].Int() + maxVal := args[1].Int() + if minVal > maxVal { + return NewNil(), fmt.Errorf("int.clamp min must be <= max") + } + n := receiver.Int() + if n < minVal { + return NewInt(minVal), nil + } + if n > maxVal { + return NewInt(maxVal), nil + } + return receiver, nil + }), nil + case "even?": + return NewAutoBuiltin("int.even?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("int.even? does not take arguments") + } + return NewBool(receiver.Int()%2 == 0), nil + }), nil + case "odd?": + return NewAutoBuiltin("int.odd?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("int.odd? does not take arguments") + } + return NewBool(receiver.Int()%2 != 0), nil + }), nil + case "times": + return NewAutoBuiltin("int.times", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("int.times does not take arguments") + } + if block.Block() == nil { + return NewNil(), fmt.Errorf("int.times requires a block") + } + count := receiver.Int() + if count <= 0 { + return receiver, nil + } + if count > int64(math.MaxInt) { + return NewNil(), fmt.Errorf("int.times value too large") + } + for i := range int(count) { + if _, err := exec.CallBlock(block, []Value{NewInt(int64(i))}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + default: + return NewNil(), exec.errorAt(pos, "unknown int member %s", property) + } + case KindFloat: + switch property { + case "abs": + return NewAutoBuiltin("float.abs", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("float.abs does not take arguments") + } + return NewFloat(math.Abs(receiver.Float())), nil + }), nil + case "clamp": + return NewAutoBuiltin("float.clamp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("float.clamp expects min and max") + } + if (args[0].Kind() != KindInt && args[0].Kind() != KindFloat) || (args[1].Kind() != KindInt && args[1].Kind() != KindFloat) { + return NewNil(), fmt.Errorf("float.clamp expects numeric min and max") + } + minVal := args[0].Float() + maxVal := args[1].Float() + if minVal > maxVal { + return NewNil(), fmt.Errorf("float.clamp min must be <= max") + } + n := receiver.Float() + if n < minVal { + return NewFloat(minVal), nil + } + if n > maxVal { + return NewFloat(maxVal), nil + } + return receiver, nil + }), nil + case "round": + return NewAutoBuiltin("float.round", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("float.round does not take arguments") + } + rounded := math.Round(receiver.Float()) + asInt, err := floatToInt64Checked(rounded, "float.round") + if err != nil { + return NewNil(), err + } + return NewInt(asInt), nil + }), nil + case "floor": + return NewAutoBuiltin("float.floor", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("float.floor does not take arguments") + } + floored := math.Floor(receiver.Float()) + asInt, err := floatToInt64Checked(floored, "float.floor") + if err != nil { + return NewNil(), err + } + return NewInt(asInt), nil + }), nil + case "ceil": + return NewAutoBuiltin("float.ceil", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("float.ceil does not take arguments") + } + ceiled := math.Ceil(receiver.Float()) + asInt, err := floatToInt64Checked(ceiled, "float.ceil") + if err != nil { + return NewNil(), err + } + return NewInt(asInt), nil + }), nil + default: + return NewNil(), exec.errorAt(pos, "unknown float member %s", property) + } + default: + return NewNil(), exec.errorAt(pos, "unsupported member access on %s", obj.Kind()) + } +} + +func moneyMember(m Money, property string) (Value, error) { + switch property { + case "currency": + return NewString(m.Currency()), nil + case "cents": + return NewInt(m.Cents()), nil + case "amount": + return NewString(m.String()), nil + case "format": + return NewAutoBuiltin("money.format", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + return NewString(m.String()), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown money member %s", property) + } +} + +func hashMember(obj Value, property string) (Value, error) { + switch property { + case "size": + return NewAutoBuiltin("hash.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.size does not take arguments") + } + return NewInt(int64(len(receiver.Hash()))), nil + }), nil + case "length": + return NewAutoBuiltin("hash.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.length does not take arguments") + } + return NewInt(int64(len(receiver.Hash()))), nil + }), nil + case "empty?": + return NewAutoBuiltin("hash.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.empty? does not take arguments") + } + return NewBool(len(receiver.Hash()) == 0), nil + }), nil + case "key?", "has_key?", "include?": + name := property + return NewAutoBuiltin("hash."+name, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("hash.%s expects exactly one key", name) + } + key, err := valueToHashKey(args[0]) + if err != nil { + return NewNil(), fmt.Errorf("hash.%s key must be symbol or string", name) + } + _, ok := receiver.Hash()[key] + return NewBool(ok), nil + }), nil + case "keys": + return NewAutoBuiltin("hash.keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.keys does not take arguments") + } + keys := sortedHashKeys(receiver.Hash()) + values := make([]Value, len(keys)) + for i, k := range keys { + values[i] = NewSymbol(k) + } + return NewArray(values), nil + }), nil + case "values": + return NewAutoBuiltin("hash.values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.values does not take arguments") + } + entries := receiver.Hash() + keys := sortedHashKeys(entries) + values := make([]Value, len(keys)) + for i, k := range keys { + values[i] = entries[k] + } + return NewArray(values), nil + }), nil + case "fetch": + return NewAutoBuiltin("hash.fetch", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("hash.fetch expects key and optional default") + } + key, err := valueToHashKey(args[0]) + if err != nil { + return NewNil(), fmt.Errorf("hash.fetch key must be symbol or string") + } + if value, ok := receiver.Hash()[key]; ok { + return value, nil + } + if len(args) == 2 { + return args[1], nil + } + return NewNil(), nil + }), nil + case "dig": + return NewAutoBuiltin("hash.dig", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) == 0 { + return NewNil(), fmt.Errorf("hash.dig expects at least one key") + } + current := receiver + for _, arg := range args { + key, err := valueToHashKey(arg) + if err != nil { + return NewNil(), fmt.Errorf("hash.dig path keys must be symbol or string") + } + if current.Kind() != KindHash && current.Kind() != KindObject { + return NewNil(), nil + } + next, ok := current.Hash()[key] + if !ok { + return NewNil(), nil + } + current = next + } + return current, nil + }), nil + case "each": + return NewAutoBuiltin("hash.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.each does not take arguments") + } + if err := ensureBlock(block, "hash.each"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + for _, key := range sortedHashKeys(entries) { + if _, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "each_key": + return NewAutoBuiltin("hash.each_key", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.each_key does not take arguments") + } + if err := ensureBlock(block, "hash.each_key"); err != nil { + return NewNil(), err + } + for _, key := range sortedHashKeys(receiver.Hash()) { + if _, err := exec.CallBlock(block, []Value{NewSymbol(key)}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "each_value": + return NewAutoBuiltin("hash.each_value", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.each_value does not take arguments") + } + if err := ensureBlock(block, "hash.each_value"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + for _, key := range sortedHashKeys(entries) { + if _, err := exec.CallBlock(block, []Value{entries[key]}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "merge": + return NewBuiltin("hash.merge", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { + return NewNil(), fmt.Errorf("hash.merge expects a single hash argument") + } + base := receiver.Hash() + addition := args[0].Hash() + out := make(map[string]Value, len(base)+len(addition)) + maps.Copy(out, base) + maps.Copy(out, addition) + return NewHash(out), nil + }), nil + case "slice": + return NewBuiltin("hash.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + entries := receiver.Hash() + out := make(map[string]Value, len(args)) + for _, arg := range args { + key, err := valueToHashKey(arg) + if err != nil { + return NewNil(), fmt.Errorf("hash.slice keys must be symbol or string") + } + if value, ok := entries[key]; ok { + out[key] = value + } + } + return NewHash(out), nil + }), nil + case "except": + return NewBuiltin("hash.except", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + excluded := make(map[string]struct{}, len(args)) + for _, arg := range args { + key, err := valueToHashKey(arg) + if err != nil { + return NewNil(), fmt.Errorf("hash.except keys must be symbol or string") + } + excluded[key] = struct{}{} + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for key, value := range entries { + if _, skip := excluded[key]; skip { + continue + } + out[key] = value + } + return NewHash(out), nil + }), nil + case "select": + return NewAutoBuiltin("hash.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.select does not take arguments") + } + if err := ensureBlock(block, "hash.select"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + include, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) + if err != nil { + return NewNil(), err + } + if include.Truthy() { + out[key] = entries[key] + } + } + return NewHash(out), nil + }), nil + case "reject": + return NewAutoBuiltin("hash.reject", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.reject does not take arguments") + } + if err := ensureBlock(block, "hash.reject"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + exclude, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) + if err != nil { + return NewNil(), err + } + if !exclude.Truthy() { + out[key] = entries[key] + } + } + return NewHash(out), nil + }), nil + case "transform_keys": + return NewAutoBuiltin("hash.transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.transform_keys does not take arguments") + } + if err := ensureBlock(block, "hash.transform_keys"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + nextKey, err := exec.CallBlock(block, []Value{NewSymbol(key)}) + if err != nil { + return NewNil(), err + } + resolved, err := valueToHashKey(nextKey) + if err != nil { + return NewNil(), fmt.Errorf("hash.transform_keys block must return symbol or string") + } + out[resolved] = entries[key] + } + return NewHash(out), nil + }), nil + case "deep_transform_keys": + return NewAutoBuiltin("hash.deep_transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.deep_transform_keys does not take arguments") + } + if err := ensureBlock(block, "hash.deep_transform_keys"); err != nil { + return NewNil(), err + } + return deepTransformKeys(exec, receiver, block) + }), nil + case "remap_keys": + return NewBuiltin("hash.remap_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { + return NewNil(), fmt.Errorf("hash.remap_keys expects a key mapping hash") + } + entries := receiver.Hash() + mapping := args[0].Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + value := entries[key] + if mapped, ok := mapping[key]; ok { + nextKey, err := valueToHashKey(mapped) + if err != nil { + return NewNil(), fmt.Errorf("hash.remap_keys mapping values must be symbol or string") + } + out[nextKey] = value + continue + } + out[key] = value + } + return NewHash(out), nil + }), nil + case "transform_values": + return NewAutoBuiltin("hash.transform_values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.transform_values does not take arguments") + } + if err := ensureBlock(block, "hash.transform_values"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + nextValue, err := exec.CallBlock(block, []Value{entries[key]}) + if err != nil { + return NewNil(), err + } + out[key] = nextValue + } + return NewHash(out), nil + }), nil + case "compact": + return NewAutoBuiltin("hash.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.compact does not take arguments") + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for k, v := range entries { + if v.Kind() != KindNil { + out[k] = v + } + } + return NewHash(out), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown hash method %s", property) + } +} + +func sortedHashKeys(entries map[string]Value) []string { + keys := make([]string, 0, len(entries)) + for key := range entries { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func deepTransformKeys(exec *Execution, value Value, block Value) (Value, error) { + return deepTransformKeysWithState(exec, value, block, &deepTransformState{ + seenHashes: make(map[uintptr]struct{}), + seenArrays: make(map[uintptr]struct{}), + }) +} + +type deepTransformState struct { + seenHashes map[uintptr]struct{} + seenArrays map[uintptr]struct{} +} + +func deepTransformKeysWithState(exec *Execution, value Value, block Value, state *deepTransformState) (Value, error) { + switch value.Kind() { + case KindHash, KindObject: + entries := value.Hash() + id := reflect.ValueOf(entries).Pointer() + if id != 0 { + if _, seen := state.seenHashes[id]; seen { + return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") + } + state.seenHashes[id] = struct{}{} + defer delete(state.seenHashes, id) + } + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + nextKeyValue, err := exec.CallBlock(block, []Value{NewSymbol(key)}) + if err != nil { + return NewNil(), err + } + nextKey, err := valueToHashKey(nextKeyValue) + if err != nil { + return NewNil(), fmt.Errorf("hash.deep_transform_keys block must return symbol or string") + } + nextValue, err := deepTransformKeysWithState(exec, entries[key], block, state) + if err != nil { + return NewNil(), err + } + out[nextKey] = nextValue + } + return NewHash(out), nil + case KindArray: + items := value.Array() + id := reflect.ValueOf(items).Pointer() + if id != 0 { + if _, seen := state.seenArrays[id]; seen { + return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") + } + state.seenArrays[id] = struct{}{} + defer delete(state.seenArrays, id) + } + out := make([]Value, len(items)) + for i, item := range items { + nextValue, err := deepTransformKeysWithState(exec, item, block, state) + if err != nil { + return NewNil(), err + } + out[i] = nextValue + } + return NewArray(out), nil + default: + return value, nil + } +} + +func chompDefault(text string) string { + if strings.HasSuffix(text, "\r\n") { + return text[:len(text)-2] + } + if strings.HasSuffix(text, "\n") || strings.HasSuffix(text, "\r") { + return text[:len(text)-1] + } + return text +} + +func stringRuneIndex(text, needle string, offset int) int { + hayRunes := []rune(text) + needleRunes := []rune(needle) + if offset < 0 || offset > len(hayRunes) { + return -1 + } + if len(needleRunes) == 0 { + return offset + } + limit := len(hayRunes) - len(needleRunes) + if limit < offset { + return -1 + } + for i := offset; i <= limit; i++ { + match := true + for j := range len(needleRunes) { + if hayRunes[i+j] != needleRunes[j] { + match = false + break + } + } + if match { + return i + } + } + return -1 +} + +func stringRuneRIndex(text, needle string, offset int) int { + hayRunes := []rune(text) + needleRunes := []rune(needle) + if offset < 0 { + return -1 + } + if offset > len(hayRunes) { + offset = len(hayRunes) + } + if len(needleRunes) == 0 { + return offset + } + if len(needleRunes) > len(hayRunes) { + return -1 + } + start := offset + maxStart := len(hayRunes) - len(needleRunes) + if start > maxStart { + start = maxStart + } + for i := start; i >= 0; i-- { + match := true + for j := range len(needleRunes) { + if hayRunes[i+j] != needleRunes[j] { + match = false + break + } + } + if match { + return i + } + } + return -1 +} + +func stringRuneSlice(text string, start, length int) (string, bool) { + runes := []rune(text) + if start < 0 || start >= len(runes) { + return "", false + } + if length < 0 { + return "", false + } + remaining := len(runes) - start + if length >= remaining { + return string(runes[start:]), true + } + end := start + length + return string(runes[start:end]), true +} + +func stringCapitalize(text string) string { + runes := []rune(text) + if len(runes) == 0 { + return "" + } + runes[0] = unicode.ToUpper(runes[0]) + for i := 1; i < len(runes); i++ { + runes[i] = unicode.ToLower(runes[i]) + } + return string(runes) +} + +func stringSwapCase(text string) string { + runes := []rune(text) + for i, r := range runes { + if unicode.IsUpper(r) { + runes[i] = unicode.ToLower(r) + continue + } + if unicode.IsLower(r) { + runes[i] = unicode.ToUpper(r) + } + } + return string(runes) +} + +func stringReverse(text string) string { + runes := []rune(text) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} + +func stringRegexOption(method string, kwargs map[string]Value) (bool, error) { + if len(kwargs) == 0 { + return false, nil + } + regexVal, ok := kwargs["regex"] + if !ok || len(kwargs) > 1 { + return false, fmt.Errorf("string.%s supports only regex keyword", method) + } + if regexVal.Kind() != KindBool { + return false, fmt.Errorf("string.%s regex keyword must be bool", method) + } + return regexVal.Bool(), nil +} + +func stringSub(text, pattern, replacement string, regex bool) (string, error) { + if !regex { + return strings.Replace(text, pattern, replacement, 1), nil + } + re, err := regexp.Compile(pattern) + if err != nil { + return "", err + } + loc := re.FindStringSubmatchIndex(text) + if loc == nil { + return text, nil + } + replaced := re.ExpandString(nil, replacement, text, loc) + return text[:loc[0]] + string(replaced) + text[loc[1]:], nil +} + +func stringGSub(text, pattern, replacement string, regex bool) (string, error) { + if !regex { + return strings.ReplaceAll(text, pattern, replacement), nil + } + re, err := regexp.Compile(pattern) + if err != nil { + return "", err + } + return re.ReplaceAllString(text, replacement), nil +} + +func stringBangResult(original, updated string) Value { + if updated == original { + return NewNil() + } + return NewString(updated) +} + +func stringSquish(text string) string { + return strings.Join(strings.Fields(text), " ") +} + +func stringTemplateOption(kwargs map[string]Value) (bool, error) { + if len(kwargs) == 0 { + return false, nil + } + value, ok := kwargs["strict"] + if !ok || len(kwargs) != 1 { + return false, fmt.Errorf("string.template supports only strict keyword") + } + if value.Kind() != KindBool { + return false, fmt.Errorf("string.template strict keyword must be bool") + } + return value.Bool(), nil +} + +func stringTemplateLookup(context Value, keyPath string) (Value, bool) { + current := context + for _, segment := range strings.Split(keyPath, ".") { + if segment == "" { + return NewNil(), false + } + if current.Kind() != KindHash && current.Kind() != KindObject { + return NewNil(), false + } + next, ok := current.Hash()[segment] + if !ok { + return NewNil(), false + } + current = next + } + return current, true +} + +func stringTemplateScalarValue(value Value, keyPath string) (string, error) { + switch value.Kind() { + case KindNil, KindBool, KindInt, KindFloat, KindString, KindSymbol, KindMoney, KindDuration, KindTime: + return value.String(), nil + default: + return "", fmt.Errorf("string.template placeholder %s value must be scalar", keyPath) + } +} + +func stringTemplate(text string, context Value, strict bool) (string, error) { + templateErr := error(nil) + rendered := stringTemplatePattern.ReplaceAllStringFunc(text, func(match string) string { + if templateErr != nil { + return match + } + submatch := stringTemplatePattern.FindStringSubmatch(match) + if len(submatch) != 2 { + return match + } + keyPath := submatch[1] + value, ok := stringTemplateLookup(context, keyPath) + if !ok { + if strict { + templateErr = fmt.Errorf("string.template missing placeholder %s", keyPath) + } + return match + } + segment, err := stringTemplateScalarValue(value, keyPath) + if err != nil { + templateErr = err + return match + } + return segment + }) + if templateErr != nil { + return "", templateErr + } + return rendered, nil +} + +func stringMember(str Value, property string) (Value, error) { + switch property { + case "size": + return NewAutoBuiltin("string.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.size does not take arguments") + } + return NewInt(int64(len([]rune(receiver.String())))), nil + }), nil + case "length": + return NewAutoBuiltin("string.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.length does not take arguments") + } + return NewInt(int64(len([]rune(receiver.String())))), nil + }), nil + case "bytesize": + return NewAutoBuiltin("string.bytesize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.bytesize does not take arguments") + } + return NewInt(int64(len(receiver.String()))), nil + }), nil + case "ord": + return NewAutoBuiltin("string.ord", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.ord does not take arguments") + } + runes := []rune(receiver.String()) + if len(runes) == 0 { + return NewNil(), fmt.Errorf("string.ord requires non-empty string") + } + return NewInt(int64(runes[0])), nil + }), nil + case "chr": + return NewAutoBuiltin("string.chr", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.chr does not take arguments") + } + runes := []rune(receiver.String()) + if len(runes) == 0 { + return NewNil(), nil + } + return NewString(string(runes[0])), nil + }), nil + case "empty?": + return NewAutoBuiltin("string.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.empty? does not take arguments") + } + return NewBool(len(receiver.String()) == 0), nil + }), nil + case "clear": + return NewAutoBuiltin("string.clear", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.clear does not take arguments") + } + return NewString(""), nil + }), nil + case "concat": + return NewAutoBuiltin("string.concat", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + var b strings.Builder + b.WriteString(receiver.String()) + for _, arg := range args { + if arg.Kind() != KindString { + return NewNil(), fmt.Errorf("string.concat expects string arguments") + } + b.WriteString(arg.String()) + } + return NewString(b.String()), nil + }), nil + case "replace": + return NewAutoBuiltin("string.replace", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.replace expects exactly one replacement") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.replace replacement must be string") + } + return NewString(args[0].String()), nil + }), nil + case "start_with?": + return NewAutoBuiltin("string.start_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.start_with? expects exactly one prefix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.start_with? prefix must be string") + } + return NewBool(strings.HasPrefix(receiver.String(), args[0].String())), nil + }), nil + case "end_with?": + return NewAutoBuiltin("string.end_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.end_with? expects exactly one suffix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.end_with? suffix must be string") + } + return NewBool(strings.HasSuffix(receiver.String(), args[0].String())), nil + }), nil + case "include?": + return NewAutoBuiltin("string.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.include? expects exactly one substring") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.include? substring must be string") + } + return NewBool(strings.Contains(receiver.String(), args[0].String())), nil + }), nil + case "match": + return NewAutoBuiltin("string.match", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("string.match does not take keyword arguments") + } + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.match expects exactly one pattern") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.match pattern must be string") + } + pattern := args[0].String() + re, err := regexp.Compile(pattern) + if err != nil { + return NewNil(), fmt.Errorf("string.match invalid regex: %v", err) + } + text := receiver.String() + indices := re.FindStringSubmatchIndex(text) + if indices == nil { + return NewNil(), nil + } + values := make([]Value, len(indices)/2) + for i := range values { + start := indices[i*2] + end := indices[i*2+1] + if start < 0 || end < 0 { + values[i] = NewNil() + continue + } + values[i] = NewString(text[start:end]) + } + return NewArray(values), nil + }), nil + case "scan": + return NewAutoBuiltin("string.scan", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("string.scan does not take keyword arguments") + } + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.scan expects exactly one pattern") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.scan pattern must be string") + } + pattern := args[0].String() + re, err := regexp.Compile(pattern) + if err != nil { + return NewNil(), fmt.Errorf("string.scan invalid regex: %v", err) + } + matches := re.FindAllString(receiver.String(), -1) + values := make([]Value, len(matches)) + for i, m := range matches { + values[i] = NewString(m) + } + return NewArray(values), nil + }), nil + case "index": + return NewAutoBuiltin("string.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("string.index expects substring and optional offset") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.index substring must be string") + } + offset := 0 + if len(args) == 2 { + i, err := valueToInt(args[1]) + if err != nil || i < 0 { + return NewNil(), fmt.Errorf("string.index offset must be non-negative integer") + } + offset = i + } + index := stringRuneIndex(receiver.String(), args[0].String(), offset) + if index < 0 { + return NewNil(), nil + } + return NewInt(int64(index)), nil + }), nil + case "rindex": + return NewAutoBuiltin("string.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("string.rindex expects substring and optional offset") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.rindex substring must be string") + } + offset := len([]rune(receiver.String())) + if len(args) == 2 { + i, err := valueToInt(args[1]) + if err != nil || i < 0 { + return NewNil(), fmt.Errorf("string.rindex offset must be non-negative integer") + } + offset = i + } + index := stringRuneRIndex(receiver.String(), args[0].String(), offset) + if index < 0 { + return NewNil(), nil + } + return NewInt(int64(index)), nil + }), nil + case "slice": + return NewAutoBuiltin("string.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("string.slice expects index and optional length") + } + start, err := valueToInt(args[0]) + if err != nil { + return NewNil(), fmt.Errorf("string.slice index must be integer") + } + runes := []rune(receiver.String()) + if len(args) == 1 { + if start < 0 || start >= len(runes) { + return NewNil(), nil + } + return NewString(string(runes[start])), nil + } + length, err := valueToInt(args[1]) + if err != nil { + return NewNil(), fmt.Errorf("string.slice length must be integer") + } + substr, ok := stringRuneSlice(receiver.String(), start, length) + if !ok { + return NewNil(), nil + } + return NewString(substr), nil + }), nil + case "strip": + return NewAutoBuiltin("string.strip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.strip does not take arguments") + } + return NewString(strings.TrimSpace(receiver.String())), nil + }), nil + case "strip!": + return NewAutoBuiltin("string.strip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.strip! does not take arguments") + } + updated := strings.TrimSpace(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "squish": + return NewAutoBuiltin("string.squish", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.squish does not take arguments") + } + return NewString(stringSquish(receiver.String())), nil + }), nil + case "squish!": + return NewAutoBuiltin("string.squish!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.squish! does not take arguments") + } + updated := stringSquish(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "lstrip": + return NewAutoBuiltin("string.lstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.lstrip does not take arguments") + } + return NewString(strings.TrimLeftFunc(receiver.String(), unicode.IsSpace)), nil + }), nil + case "lstrip!": + return NewAutoBuiltin("string.lstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.lstrip! does not take arguments") + } + updated := strings.TrimLeftFunc(receiver.String(), unicode.IsSpace) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "rstrip": + return NewAutoBuiltin("string.rstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.rstrip does not take arguments") + } + return NewString(strings.TrimRightFunc(receiver.String(), unicode.IsSpace)), nil + }), nil + case "rstrip!": + return NewAutoBuiltin("string.rstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.rstrip! does not take arguments") + } + updated := strings.TrimRightFunc(receiver.String(), unicode.IsSpace) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "chomp": + return NewAutoBuiltin("string.chomp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("string.chomp accepts at most one separator") + } + text := receiver.String() + if len(args) == 0 { + return NewString(chompDefault(text)), nil + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.chomp separator must be string") + } + sep := args[0].String() + if sep == "" { + return NewString(strings.TrimRight(text, "\r\n")), nil + } + if strings.HasSuffix(text, sep) { + return NewString(text[:len(text)-len(sep)]), nil + } + return NewString(text), nil + }), nil + case "chomp!": + return NewAutoBuiltin("string.chomp!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("string.chomp! accepts at most one separator") + } + original := receiver.String() + if len(args) == 0 { + return stringBangResult(original, chompDefault(original)), nil + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.chomp! separator must be string") + } + sep := args[0].String() + if sep == "" { + return stringBangResult(original, strings.TrimRight(original, "\r\n")), nil + } + if strings.HasSuffix(original, sep) { + return stringBangResult(original, original[:len(original)-len(sep)]), nil + } + return NewNil(), nil + }), nil + case "delete_prefix": + return NewAutoBuiltin("string.delete_prefix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_prefix expects exactly one prefix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_prefix prefix must be string") + } + return NewString(strings.TrimPrefix(receiver.String(), args[0].String())), nil + }), nil + case "delete_prefix!": + return NewAutoBuiltin("string.delete_prefix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_prefix! expects exactly one prefix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_prefix! prefix must be string") + } + updated := strings.TrimPrefix(receiver.String(), args[0].String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "delete_suffix": + return NewAutoBuiltin("string.delete_suffix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_suffix expects exactly one suffix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_suffix suffix must be string") + } + return NewString(strings.TrimSuffix(receiver.String(), args[0].String())), nil + }), nil + case "delete_suffix!": + return NewAutoBuiltin("string.delete_suffix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_suffix! expects exactly one suffix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_suffix! suffix must be string") + } + updated := strings.TrimSuffix(receiver.String(), args[0].String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "upcase": + return NewAutoBuiltin("string.upcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.upcase does not take arguments") + } + return NewString(strings.ToUpper(receiver.String())), nil + }), nil + case "upcase!": + return NewAutoBuiltin("string.upcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.upcase! does not take arguments") + } + updated := strings.ToUpper(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "downcase": + return NewAutoBuiltin("string.downcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.downcase does not take arguments") + } + return NewString(strings.ToLower(receiver.String())), nil + }), nil + case "downcase!": + return NewAutoBuiltin("string.downcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.downcase! does not take arguments") + } + updated := strings.ToLower(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "capitalize": + return NewAutoBuiltin("string.capitalize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.capitalize does not take arguments") + } + return NewString(stringCapitalize(receiver.String())), nil + }), nil + case "capitalize!": + return NewAutoBuiltin("string.capitalize!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.capitalize! does not take arguments") + } + updated := stringCapitalize(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "swapcase": + return NewAutoBuiltin("string.swapcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.swapcase does not take arguments") + } + return NewString(stringSwapCase(receiver.String())), nil + }), nil + case "swapcase!": + return NewAutoBuiltin("string.swapcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.swapcase! does not take arguments") + } + updated := stringSwapCase(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "reverse": + return NewAutoBuiltin("string.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.reverse does not take arguments") + } + return NewString(stringReverse(receiver.String())), nil + }), nil + case "reverse!": + return NewAutoBuiltin("string.reverse!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.reverse! does not take arguments") + } + updated := stringReverse(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "sub": + return NewAutoBuiltin("string.sub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.sub expects pattern and replacement") + } + regex, err := stringRegexOption("sub", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub replacement must be string") + } + updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.sub invalid regex: %v", err) + } + return NewString(updated), nil + }), nil + case "sub!": + return NewAutoBuiltin("string.sub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.sub! expects pattern and replacement") + } + regex, err := stringRegexOption("sub!", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub! pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub! replacement must be string") + } + updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.sub! invalid regex: %v", err) + } + return stringBangResult(receiver.String(), updated), nil + }), nil + case "gsub": + return NewAutoBuiltin("string.gsub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.gsub expects pattern and replacement") + } + regex, err := stringRegexOption("gsub", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub replacement must be string") + } + updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.gsub invalid regex: %v", err) + } + return NewString(updated), nil + }), nil + case "gsub!": + return NewAutoBuiltin("string.gsub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.gsub! expects pattern and replacement") + } + regex, err := stringRegexOption("gsub!", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub! pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub! replacement must be string") + } + updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.gsub! invalid regex: %v", err) + } + return stringBangResult(receiver.String(), updated), nil + }), nil + case "split": + return NewAutoBuiltin("string.split", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("string.split accepts at most one separator") + } + text := receiver.String() + var parts []string + if len(args) == 0 { + parts = strings.Fields(text) + } else { + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.split separator must be string") + } + parts = strings.Split(text, args[0].String()) + } + values := make([]Value, len(parts)) + for i, part := range parts { + values[i] = NewString(part) + } + return NewArray(values), nil + }), nil + case "template": + return NewAutoBuiltin("string.template", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.template expects exactly one context hash") + } + if args[0].Kind() != KindHash && args[0].Kind() != KindObject { + return NewNil(), fmt.Errorf("string.template context must be hash") + } + strict, err := stringTemplateOption(kwargs) + if err != nil { + return NewNil(), err + } + rendered, err := stringTemplate(receiver.String(), args[0], strict) + if err != nil { + return NewNil(), err + } + return NewString(rendered), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown string method %s", property) + } +} + +func durationMember(d Duration, property string, pos Position) (Value, error) { + switch property { + case "seconds", "second": + return NewInt(d.Seconds()), nil + case "minutes", "minute": + return NewInt(d.Seconds() / 60), nil + case "hours", "hour": + return NewInt(d.Seconds() / 3600), nil + case "days", "day": + return NewInt(d.Seconds() / 86400), nil + case "weeks", "week": + return NewInt(d.Seconds() / 604800), nil + case "in_seconds": + return NewFloat(float64(d.Seconds())), nil + case "in_minutes": + return NewFloat(float64(d.Seconds()) / 60), nil + case "in_hours": + return NewFloat(float64(d.Seconds()) / 3600), nil + case "in_days": + return NewFloat(float64(d.Seconds()) / 86400), nil + case "in_weeks": + return NewFloat(float64(d.Seconds()) / 604800), nil + case "in_months": + return NewFloat(float64(d.Seconds()) / (30 * 86400)), nil + case "in_years": + return NewFloat(float64(d.Seconds()) / (365 * 86400)), nil + case "iso8601": + return NewString(d.iso8601()), nil + case "parts": + p := d.parts() + return NewHash(map[string]Value{ + "days": NewInt(p["days"]), + "hours": NewInt(p["hours"]), + "minutes": NewInt(p["minutes"]), + "seconds": NewInt(p["seconds"]), + }), nil + case "to_i": + return NewInt(d.Seconds()), nil + case "to_s": + return NewString(d.String()), nil + case "format": + return NewString(d.String()), nil + case "eql?": + return NewBuiltin("duration.eql?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 || args[0].Kind() != KindDuration { + return NewNil(), fmt.Errorf("duration.eql? expects a duration") + } + return NewBool(d.Seconds() == args[0].Duration().Seconds()), nil + }), nil + case "after", "since", "from_now": + return NewBuiltin("duration.after", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + start, err := durationTimeArg(args, true, "after") + if err != nil { + return NewNil(), err + } + result := start.Add(time.Duration(d.Seconds()) * time.Second).UTC() + return NewTime(result), nil + }), nil + case "ago", "before", "until": + return NewBuiltin("duration.before", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + start, err := durationTimeArg(args, true, "before") + if err != nil { + return NewNil(), err + } + result := start.Add(-time.Duration(d.Seconds()) * time.Second).UTC() + return NewTime(result), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown duration method %s", property) + } +} + +func durationTimeArg(args []Value, allowEmpty bool, name string) (time.Time, error) { + if len(args) == 0 { + if allowEmpty { + return time.Now().UTC(), nil + } + return time.Time{}, fmt.Errorf("%s expects a time argument", name) + } + if len(args) != 1 { + return time.Time{}, fmt.Errorf("%s expects at most one time argument", name) + } + val := args[0] + switch val.Kind() { + case KindString: + t, err := time.Parse(time.RFC3339, val.String()) + if err != nil { + return time.Time{}, fmt.Errorf("invalid time: %v", err) + } + return t.UTC(), nil + case KindTime: + return val.Time(), nil + default: + return time.Time{}, fmt.Errorf("%s expects a Time or RFC3339 string", name) + } +} + +func arrayMember(array Value, property string) (Value, error) { + switch property { + case "size": + return NewAutoBuiltin("array.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.size does not take arguments") + } + return NewInt(int64(len(receiver.Array()))), nil + }), nil + case "each": + return NewAutoBuiltin("array.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.each"); err != nil { + return NewNil(), err + } + for _, item := range receiver.Array() { + if _, err := exec.CallBlock(block, []Value{item}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "map": + return NewAutoBuiltin("array.map", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.map"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + result := make([]Value, len(arr)) + for i, item := range arr { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + result[i] = val + } + return NewArray(result), nil + }), nil + case "select": + return NewAutoBuiltin("array.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.select"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + out := make([]Value, 0, len(arr)) + for _, item := range arr { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if val.Truthy() { + out = append(out, item) + } + } + return NewArray(out), nil + }), nil + case "find": + return NewAutoBuiltin("array.find", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.find does not take arguments") + } + if err := ensureBlock(block, "array.find"); err != nil { + return NewNil(), err + } + for _, item := range receiver.Array() { + match, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if match.Truthy() { + return item, nil + } + } + return NewNil(), nil + }), nil + case "find_index": + return NewAutoBuiltin("array.find_index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.find_index does not take arguments") + } + if err := ensureBlock(block, "array.find_index"); err != nil { + return NewNil(), err + } + for idx, item := range receiver.Array() { + match, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if match.Truthy() { + return NewInt(int64(idx)), nil + } + } + return NewNil(), nil + }), nil + case "reduce": + return NewAutoBuiltin("array.reduce", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.reduce"); err != nil { + return NewNil(), err + } + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.reduce accepts at most one initial value") + } + arr := receiver.Array() + if len(arr) == 0 && len(args) == 0 { + return NewNil(), fmt.Errorf("array.reduce on empty array requires an initial value") + } + var acc Value + start := 0 + if len(args) == 1 { + acc = args[0] + } else { + acc = arr[0] + start = 1 + } + for i := start; i < len(arr); i++ { + next, err := exec.CallBlock(block, []Value{acc, arr[i]}) + if err != nil { + return NewNil(), err + } + acc = next + } + return acc, nil + }), nil + case "include?": + return NewAutoBuiltin("array.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("array.include? expects exactly one value") + } + for _, item := range receiver.Array() { + if item.Equal(args[0]) { + return NewBool(true), nil + } + } + return NewBool(false), nil + }), nil + case "index": + return NewAutoBuiltin("array.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("array.index expects value and optional offset") + } + offset := 0 + if len(args) == 2 { + n, err := valueToInt(args[1]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.index offset must be non-negative integer") + } + offset = n + } + arr := receiver.Array() + if offset >= len(arr) { + return NewNil(), nil + } + for idx := offset; idx < len(arr); idx++ { + if arr[idx].Equal(args[0]) { + return NewInt(int64(idx)), nil + } + } + return NewNil(), nil + }), nil + case "rindex": + return NewAutoBuiltin("array.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("array.rindex expects value and optional offset") + } + offset := -1 + if len(args) == 2 { + n, err := valueToInt(args[1]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.rindex offset must be non-negative integer") + } + offset = n + } + arr := receiver.Array() + if len(arr) == 0 { + return NewNil(), nil + } + if offset < 0 { + offset = len(arr) - 1 + } + if offset >= len(arr) { + offset = len(arr) - 1 + } + for idx := offset; idx >= 0; idx-- { + if arr[idx].Equal(args[0]) { + return NewInt(int64(idx)), nil + } + } + return NewNil(), nil + }), nil + case "count": + return NewAutoBuiltin("array.count", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.count accepts at most one value argument") + } + arr := receiver.Array() + if len(args) == 1 { + if block.Block() != nil { + return NewNil(), fmt.Errorf("array.count does not accept both argument and block") + } + total := int64(0) + for _, item := range arr { + if item.Equal(args[0]) { + total++ + } + } + return NewInt(total), nil + } + if block.Block() == nil { + return NewInt(int64(len(arr))), nil + } + total := int64(0) + for _, item := range arr { + include, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if include.Truthy() { + total++ + } + } + return NewInt(total), nil + }), nil + case "any?": + return NewAutoBuiltin("array.any?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.any? does not take arguments") + } + for _, item := range receiver.Array() { + if block.Block() != nil { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if val.Truthy() { + return NewBool(true), nil + } + continue + } + if item.Truthy() { + return NewBool(true), nil + } + } + return NewBool(false), nil + }), nil + case "all?": + return NewAutoBuiltin("array.all?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.all? does not take arguments") + } + for _, item := range receiver.Array() { + if block.Block() != nil { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if !val.Truthy() { + return NewBool(false), nil + } + continue + } + if !item.Truthy() { + return NewBool(false), nil + } + } + return NewBool(true), nil + }), nil + case "none?": + return NewAutoBuiltin("array.none?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.none? does not take arguments") + } + for _, item := range receiver.Array() { + if block.Block() != nil { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if val.Truthy() { + return NewBool(false), nil + } + continue + } + if item.Truthy() { + return NewBool(false), nil + } + } + return NewBool(true), nil + }), nil + case "push": + return NewBuiltin("array.push", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) == 0 { + return NewNil(), fmt.Errorf("array.push expects at least one argument") + } + base := receiver.Array() + out := make([]Value, len(base)+len(args)) + copy(out, base) + copy(out[len(base):], args) + return NewArray(out), nil + }), nil + case "pop": + return NewAutoBuiltin("array.pop", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.pop accepts at most one argument") + } + count := 1 + if len(args) == 1 { + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.pop expects non-negative integer") + } + count = n + } + arr := receiver.Array() + if count == 0 { + return NewHash(map[string]Value{ + "array": NewArray(arr), + "popped": NewNil(), + }), nil + } + if len(arr) == 0 { + popped := NewNil() + if len(args) == 1 { + popped = NewArray([]Value{}) + } + return NewHash(map[string]Value{ + "array": NewArray([]Value{}), + "popped": popped, + }), nil + } + if count > len(arr) { + count = len(arr) + } + remaining := make([]Value, len(arr)-count) + copy(remaining, arr[:len(arr)-count]) + removed := make([]Value, count) + copy(removed, arr[len(arr)-count:]) + result := map[string]Value{ + "array": NewArray(remaining), + } + if count == 1 && len(args) == 0 { + result["popped"] = removed[0] + } else { + result["popped"] = NewArray(removed) + } + return NewHash(result), nil + }), nil + case "uniq": + return NewAutoBuiltin("array.uniq", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.uniq does not take arguments") + } + arr := receiver.Array() + unique := make([]Value, 0, len(arr)) + for _, item := range arr { + found := slices.ContainsFunc(unique, item.Equal) + if !found { + unique = append(unique, item) + } + } + return NewArray(unique), nil + }), nil + case "first": + return NewAutoBuiltin("array.first", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + arr := receiver.Array() + if len(args) == 0 { + if len(arr) == 0 { + return NewNil(), nil + } + return arr[0], nil + } + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.first expects non-negative integer") + } + if n > len(arr) { + n = len(arr) + } + out := make([]Value, n) + copy(out, arr[:n]) + return NewArray(out), nil + }), nil + case "last": + return NewAutoBuiltin("array.last", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + arr := receiver.Array() + if len(args) == 0 { + if len(arr) == 0 { + return NewNil(), nil + } + return arr[len(arr)-1], nil + } + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.last expects non-negative integer") + } + if n > len(arr) { + n = len(arr) + } + out := make([]Value, n) + copy(out, arr[len(arr)-n:]) + return NewArray(out), nil + }), nil + case "sum": + return NewAutoBuiltin("array.sum", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + arr := receiver.Array() + total := NewInt(0) + for _, item := range arr { + switch item.Kind() { + case KindInt, KindFloat: + default: + return NewNil(), fmt.Errorf("array.sum supports numeric values") + } + sum, err := addValues(total, item) + if err != nil { + return NewNil(), fmt.Errorf("array.sum supports numeric values") + } + total = sum + } + return total, nil + }), nil + case "compact": + return NewAutoBuiltin("array.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.compact does not take arguments") + } + arr := receiver.Array() + out := make([]Value, 0, len(arr)) + for _, item := range arr { + if item.Kind() != KindNil { + out = append(out, item) + } + } + return NewArray(out), nil + }), nil + case "flatten": + return NewAutoBuiltin("array.flatten", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + // depth=-1 is a sentinel value meaning "flatten fully" (no depth limit) + depth := -1 + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.flatten accepts at most one depth argument") + } + if len(args) == 1 { + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.flatten depth must be non-negative integer") + } + depth = n + } + arr := receiver.Array() + out := flattenValues(arr, depth) + return NewArray(out), nil + }), nil + case "chunk": + return NewAutoBuiltin("array.chunk", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("array.chunk expects a chunk size") + } + sizeValue := args[0] + maxNativeInt := int64(^uint(0) >> 1) + if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { + return NewNil(), fmt.Errorf("array.chunk size must be a positive integer") + } + size := int(sizeValue.Int()) + arr := receiver.Array() + if len(arr) == 0 { + return NewArray([]Value{}), nil + } + chunkCapacity := len(arr) / size + if len(arr)%size != 0 { + chunkCapacity++ + } + chunks := make([]Value, 0, chunkCapacity) + for i := 0; i < len(arr); i += size { + end := i + size + if end > len(arr) { + end = len(arr) + } + part := make([]Value, end-i) + copy(part, arr[i:end]) + chunks = append(chunks, NewArray(part)) + } + return NewArray(chunks), nil + }), nil + case "window": + return NewAutoBuiltin("array.window", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("array.window expects a window size") + } + sizeValue := args[0] + maxNativeInt := int64(^uint(0) >> 1) + if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { + return NewNil(), fmt.Errorf("array.window size must be a positive integer") + } + size := int(sizeValue.Int()) + arr := receiver.Array() + if size > len(arr) { + return NewArray([]Value{}), nil + } + windows := make([]Value, 0, len(arr)-size+1) + for i := 0; i+size <= len(arr); i++ { + part := make([]Value, size) + copy(part, arr[i:i+size]) + windows = append(windows, NewArray(part)) + } + return NewArray(windows), nil + }), nil + case "join": + return NewAutoBuiltin("array.join", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.join accepts at most one separator") + } + sep := "" + if len(args) == 1 { + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("array.join separator must be string") + } + sep = args[0].String() + } + arr := receiver.Array() + if len(arr) == 0 { + return NewString(""), nil + } + // Use strings.Builder for efficient concatenation + var b strings.Builder + for i, item := range arr { + if i > 0 { + b.WriteString(sep) + } + b.WriteString(item.String()) + } + return NewString(b.String()), nil + }), nil + case "reverse": + return NewAutoBuiltin("array.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.reverse does not take arguments") + } + arr := receiver.Array() + out := make([]Value, len(arr)) + for i, item := range arr { + out[len(arr)-1-i] = item + } + return NewArray(out), nil + }), nil + case "sort": + return NewAutoBuiltin("array.sort", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.sort does not take arguments") + } + arr := receiver.Array() + out := make([]Value, len(arr)) + copy(out, arr) + var sortErr error + sort.SliceStable(out, func(i, j int) bool { + if sortErr != nil { + return false + } + if block.Block() != nil { + cmpValue, err := exec.CallBlock(block, []Value{out[i], out[j]}) + if err != nil { + sortErr = err + return false + } + cmp, err := sortComparisonResult(cmpValue) + if err != nil { + sortErr = fmt.Errorf("array.sort block must return numeric comparator") + return false + } + return cmp < 0 + } + cmp, err := arraySortCompareValues(out[i], out[j]) + if err != nil { + sortErr = fmt.Errorf("array.sort values are not comparable") + return false + } + return cmp < 0 + }) + if sortErr != nil { + return NewNil(), sortErr + } + return NewArray(out), nil + }), nil + case "sort_by": + return NewAutoBuiltin("array.sort_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.sort_by does not take arguments") + } + if err := ensureBlock(block, "array.sort_by"); err != nil { + return NewNil(), err + } + type itemWithSortKey struct { + item Value + key Value + index int + } + arr := receiver.Array() + withKeys := make([]itemWithSortKey, len(arr)) + for i, item := range arr { + sortKey, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + withKeys[i] = itemWithSortKey{item: item, key: sortKey, index: i} + } + var sortErr error + sort.SliceStable(withKeys, func(i, j int) bool { + if sortErr != nil { + return false + } + cmp, err := arraySortCompareValues(withKeys[i].key, withKeys[j].key) + if err != nil { + sortErr = fmt.Errorf("array.sort_by block values are not comparable") + return false + } + if cmp == 0 { + return withKeys[i].index < withKeys[j].index + } + return cmp < 0 + }) + if sortErr != nil { + return NewNil(), sortErr + } + out := make([]Value, len(withKeys)) + for i, item := range withKeys { + out[i] = item.item + } + return NewArray(out), nil + }), nil + case "partition": + return NewAutoBuiltin("array.partition", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.partition does not take arguments") + } + if err := ensureBlock(block, "array.partition"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + left := make([]Value, 0, len(arr)) + right := make([]Value, 0, len(arr)) + for _, item := range arr { + match, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if match.Truthy() { + left = append(left, item) + } else { + right = append(right, item) + } + } + return NewArray([]Value{NewArray(left), NewArray(right)}), nil + }), nil + case "group_by": + return NewAutoBuiltin("array.group_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.group_by does not take arguments") + } + if err := ensureBlock(block, "array.group_by"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + groups := make(map[string][]Value, len(arr)) + for _, item := range arr { + groupValue, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + key, err := valueToHashKey(groupValue) + if err != nil { + return NewNil(), fmt.Errorf("array.group_by block must return symbol or string") + } + groups[key] = append(groups[key], item) + } + result := make(map[string]Value, len(groups)) + for key, items := range groups { + result[key] = NewArray(items) + } + return NewHash(result), nil + }), nil + case "group_by_stable": + return NewAutoBuiltin("array.group_by_stable", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.group_by_stable does not take arguments") + } + if err := ensureBlock(block, "array.group_by_stable"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + order := make([]string, 0, len(arr)) + keyValues := make(map[string]Value, len(arr)) + groups := make(map[string][]Value, len(arr)) + for _, item := range arr { + groupValue, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + key, err := valueToHashKey(groupValue) + if err != nil { + return NewNil(), fmt.Errorf("array.group_by_stable block must return symbol or string") + } + if _, exists := groups[key]; !exists { + order = append(order, key) + keyValues[key] = groupValue + groups[key] = []Value{} + } + groups[key] = append(groups[key], item) + } + result := make([]Value, 0, len(order)) + for _, key := range order { + result = append(result, NewArray([]Value{ + keyValues[key], + NewArray(groups[key]), + })) + } + return NewArray(result), nil + }), nil + case "tally": + return NewAutoBuiltin("array.tally", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.tally does not take arguments") + } + arr := receiver.Array() + counts := make(map[string]int64, len(arr)) + for _, item := range arr { + keyValue := item + if block.Block() != nil { + mapped, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + keyValue = mapped + } + key, err := valueToHashKey(keyValue) + if err != nil { + return NewNil(), fmt.Errorf("array.tally values must be symbol or string") + } + counts[key]++ + } + result := make(map[string]Value, len(counts)) + for key, count := range counts { + result[key] = NewInt(count) + } + return NewHash(result), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown array method %s", property) + } +} From fc9e405dc4f57810ead483b566195916583bfa97 Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:12:55 -0500 Subject: [PATCH 09/99] refactor parser expression handling into dedicated file --- vibes/parser.go | 490 ----------------------------------- vibes/parser_expressions.go | 496 ++++++++++++++++++++++++++++++++++++ 2 files changed, 496 insertions(+), 490 deletions(-) create mode 100644 vibes/parser_expressions.go diff --git a/vibes/parser.go b/vibes/parser.go index db7ebdd..a03fcac 100644 --- a/vibes/parser.go +++ b/vibes/parser.go @@ -2,7 +2,6 @@ package vibes import ( "fmt" - "strconv" "strings" ) @@ -116,495 +115,6 @@ func (p *parser) ParseProgram() (*Program, []error) { return program, p.errors } -func (p *parser) parseExpression(precedence int) Expression { - prefix := p.prefixFns[p.curToken.Type] - if prefix == nil { - p.errorUnexpected(p.curToken) - return nil - } - - left := prefix() - if left == nil { - return nil - } - - for p.peekToken.Type != tokenEOF && precedence < p.peekPrecedence() { - infix := p.infixFns[p.peekToken.Type] - if infix == nil { - return left - } - p.nextToken() - left = infix(left) - if left == nil { - return nil - } - } - - return left -} - -func (p *parser) parseIdentifier() Expression { - return &Identifier{Name: p.curToken.Literal, position: p.curToken.Pos} -} - -func (p *parser) parseIntegerLiteral() Expression { - value, err := strconv.ParseInt(p.curToken.Literal, 10, 64) - if err != nil { - p.addParseError(p.curToken.Pos, "invalid integer literal") - return nil - } - return &IntegerLiteral{Value: value, position: p.curToken.Pos} -} - -func (p *parser) parseFloatLiteral() Expression { - value, err := strconv.ParseFloat(p.curToken.Literal, 64) - if err != nil { - p.addParseError(p.curToken.Pos, "invalid float literal") - return nil - } - return &FloatLiteral{Value: value, position: p.curToken.Pos} -} - -func (p *parser) parseStringLiteral() Expression { - return &StringLiteral{Value: p.curToken.Literal, position: p.curToken.Pos} -} - -func (p *parser) parseBooleanLiteral() Expression { - return &BoolLiteral{Value: p.curToken.Type == tokenTrue, position: p.curToken.Pos} -} - -func (p *parser) parseNilLiteral() Expression { - return &NilLiteral{position: p.curToken.Pos} -} - -func (p *parser) parseSymbolLiteral() Expression { - return &SymbolLiteral{Name: p.curToken.Literal, position: p.curToken.Pos} -} - -func (p *parser) parseIvarLiteral() Expression { - return &IvarExpr{Name: p.curToken.Literal, position: p.curToken.Pos} -} - -func (p *parser) parseClassVarLiteral() Expression { - return &ClassVarExpr{Name: p.curToken.Literal, position: p.curToken.Pos} -} - -func (p *parser) parseSelfLiteral() Expression { - return &Identifier{Name: "self", position: p.curToken.Pos} -} - -func (p *parser) parseYieldExpression() Expression { - pos := p.curToken.Pos - var args []Expression - if p.peekToken.Type == tokenLParen { - p.nextToken() - p.nextToken() - if p.curToken.Type != tokenRParen { - args = append(args, p.parseExpression(lowestPrec)) - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - args = append(args, p.parseExpression(lowestPrec)) - } - if !p.expectPeek(tokenRParen) { - return nil - } - } - } else if p.prefixFns[p.peekToken.Type] != nil { - p.nextToken() - args = append(args, p.parseExpression(lowestPrec)) - } - return &YieldExpr{Args: args, position: pos} -} - -func (p *parser) parseCaseExpression() Expression { - pos := p.curToken.Pos - p.nextToken() - target := p.parseExpression(lowestPrec) - if target == nil { - return nil - } - - p.nextToken() - clauses := []CaseWhenClause{} - for p.curToken.Type == tokenWhen { - p.nextToken() - values := []Expression{} - first := p.parseExpression(lowestPrec) - if first == nil { - return nil - } - values = append(values, first) - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - value := p.parseExpression(lowestPrec) - if value == nil { - return nil - } - values = append(values, value) - } - - p.nextToken() - result := p.parseExpressionWithBlock() - if result == nil { - return nil - } - clauses = append(clauses, CaseWhenClause{Values: values, Result: result}) - p.nextToken() - } - - if len(clauses) == 0 { - p.errorExpected(p.curToken, "when") - return nil - } - - var elseExpr Expression - if p.curToken.Type == tokenElse { - p.nextToken() - elseExpr = p.parseExpressionWithBlock() - if elseExpr == nil { - return nil - } - p.nextToken() - } - - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - return nil - } - - return &CaseExpr{Target: target, Clauses: clauses, ElseExpr: elseExpr, position: pos} -} - -func (p *parser) parseGroupedExpression() Expression { - p.nextToken() - expr := p.parseExpression(lowestPrec) - if !p.expectPeek(tokenRParen) { - return nil - } - return expr -} - -func (p *parser) parseArrayLiteral() Expression { - pos := p.curToken.Pos - elements := []Expression{} - - if p.peekToken.Type == tokenRBracket { - p.nextToken() - return &ArrayLiteral{Elements: elements, position: pos} - } - - p.nextToken() - elements = append(elements, p.parseExpression(lowestPrec)) - - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - elements = append(elements, p.parseExpression(lowestPrec)) - } - - if !p.expectPeek(tokenRBracket) { - return nil - } - - return &ArrayLiteral{Elements: elements, position: pos} -} - -func (p *parser) parseHashLiteral() Expression { - pos := p.curToken.Pos - pairs := []HashPair{} - - if p.peekToken.Type == tokenRBrace { - p.nextToken() - return &HashLiteral{Pairs: pairs, position: pos} - } - - p.nextToken() - if pair := p.parseHashPair(); pair.Key != nil { - pairs = append(pairs, pair) - } - - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - if pair := p.parseHashPair(); pair.Key != nil { - pairs = append(pairs, pair) - } - } - - if !p.expectPeek(tokenRBrace) { - return nil - } - - return &HashLiteral{Pairs: pairs, position: pos} -} - -func (p *parser) parseHashPair() HashPair { - if !isLabelNameToken(p.curToken.Type) || p.peekToken.Type != tokenColon { - p.addParseError(p.curToken.Pos, "invalid hash pair: expected symbol-style key like name:") - return HashPair{} - } - - key := &SymbolLiteral{Name: p.curToken.Literal, position: p.curToken.Pos} - p.nextToken() - p.nextToken() - if p.curToken.Type == tokenComma || p.curToken.Type == tokenRBrace { - p.addParseError(p.curToken.Pos, fmt.Sprintf("missing value for hash key %s", key.Name)) - return HashPair{} - } - - value := p.parseExpression(lowestPrec) - if value == nil { - return HashPair{} - } - return HashPair{Key: key, Value: value} -} - -func (p *parser) parsePrefixExpression() Expression { - pos := p.curToken.Pos - operator := p.curToken.Type - p.nextToken() - right := p.parseExpression(precPrefix) - return &UnaryExpr{Operator: operator, Right: right, position: pos} -} - -func (p *parser) parseInfixExpression(left Expression) Expression { - pos := p.curToken.Pos - operator := p.curToken.Type - precedence := p.curPrecedence() - p.nextToken() - right := p.parseExpression(precedence) - return &BinaryExpr{Left: left, Operator: operator, Right: right, position: pos} -} - -func (p *parser) parseRangeExpression(left Expression) Expression { - pos := p.curToken.Pos - precedence := p.curPrecedence() - p.nextToken() - right := p.parseExpression(precedence) - return &RangeExpr{Start: left, End: right, position: pos} -} - -func (p *parser) parseCallExpression(function Expression) Expression { - if function == nil { - return nil - } - expr := &CallExpr{Callee: function, position: function.Pos()} - args := []Expression{} - kwargs := []KeywordArg{} - - if p.peekToken.Type == tokenRParen { - p.nextToken() - expr.Args = args - expr.KwArgs = kwargs - return expr - } - - p.nextToken() - p.parseCallArgument(&args, &kwargs) - - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - p.parseCallArgument(&args, &kwargs) - } - - if !p.expectPeek(tokenRParen) { - return nil - } - - expr.Args = args - expr.KwArgs = kwargs - if p.peekToken.Type == tokenDo { - p.nextToken() - expr.Block = p.parseBlockLiteral() - } - return expr -} - -func (p *parser) parseCallArgument(args *[]Expression, kwargs *[]KeywordArg) { - if isLabelNameToken(p.curToken.Type) && p.peekToken.Type == tokenColon { - name := p.curToken.Literal - p.nextToken() - p.nextToken() - if p.curToken.Type == tokenComma || p.curToken.Type == tokenRParen { - p.addParseError(p.curToken.Pos, fmt.Sprintf("missing value for keyword argument %s", name)) - return - } - value := p.parseExpression(lowestPrec) - if value == nil { - return - } - *kwargs = append(*kwargs, KeywordArg{Name: name, Value: value}) - return - } - - expr := p.parseExpression(lowestPrec) - if expr != nil { - *args = append(*args, expr) - } -} - -func isLabelNameToken(tt TokenType) bool { - switch tt { - case tokenIdent, - tokenDef, tokenClass, tokenSelf, tokenPrivate, tokenProperty, tokenGetter, tokenSetter, - tokenEnd, tokenReturn, tokenYield, tokenDo, tokenFor, tokenWhile, tokenUntil, - tokenBreak, tokenNext, tokenIn, tokenIf, tokenCase, tokenWhen, tokenElsif, tokenElse, - tokenTrue, tokenFalse, tokenNil: - return true - default: - return false - } -} - -func (p *parser) parseBlockLiteral() *BlockLiteral { - pos := p.curToken.Pos - params := []Param{} - - p.nextToken() - if p.curToken.Type == tokenPipe { - var ok bool - params, ok = p.parseBlockParameters() - if !ok { - return nil - } - p.nextToken() - } - - body := p.parseBlock(tokenEnd) - if p.curToken.Type != tokenEnd { - p.errorExpected(p.curToken, "end") - } - - return &BlockLiteral{Params: params, Body: body, position: pos} -} - -func (p *parser) parseBlockParameters() ([]Param, bool) { - params := []Param{} - p.nextToken() - if p.curToken.Type == tokenPipe { - return params, true - } - - param, ok := p.parseBlockParameter() - if !ok { - return nil, false - } - params = append(params, param) - - for p.peekToken.Type == tokenComma { - p.nextToken() - p.nextToken() - if p.curToken.Type == tokenPipe { - p.addParseError(p.curToken.Pos, "trailing comma in block parameter list") - return nil, false - } - param, ok := p.parseBlockParameter() - if !ok { - return nil, false - } - params = append(params, param) - } - - if !p.expectPeek(tokenPipe) { - return nil, false - } - - return params, true -} - -func (p *parser) parseBlockParameter() (Param, bool) { - if p.curToken.Type != tokenIdent { - p.errorExpected(p.curToken, "block parameter") - return Param{}, false - } - param := Param{Name: p.curToken.Literal} - if p.peekToken.Type == tokenColon { - p.nextToken() - p.nextToken() - param.Type = p.parseBlockParamType() - if param.Type == nil { - return Param{}, false - } - } - return param, true -} - -func (p *parser) parseBlockParamType() *TypeExpr { - first := p.parseTypeAtom() - if first == nil { - return nil - } - - union := []*TypeExpr{first} - for p.peekToken.Type == tokenPipe && p.blockParamUnionContinues() { - p.nextToken() - p.nextToken() - next := p.parseTypeAtom() - if next == nil { - return nil - } - union = append(union, next) - } - - if len(union) == 1 { - return first - } - - names := make([]string, len(union)) - for i, option := range union { - names[i] = formatTypeExpr(option) - } - return &TypeExpr{ - Name: strings.Join(names, " | "), - Kind: TypeUnion, - Union: union, - position: first.position, - } -} - -func (p *parser) blockParamUnionContinues() bool { - if p.peekToken.Type != tokenPipe { - return false - } - - savedLexer := *p.l - savedCur := p.curToken - savedPeek := p.peekToken - savedErrors := len(p.errors) - - p.nextToken() - p.nextToken() - atom := p.parseTypeAtom() - ok := atom != nil && (p.peekToken.Type == tokenComma || p.peekToken.Type == tokenPipe) - - p.l = &savedLexer - p.curToken = savedCur - p.peekToken = savedPeek - p.errors = p.errors[:savedErrors] - return ok -} - -func (p *parser) parseMemberExpression(object Expression) Expression { - if object == nil { - return nil - } - p.nextToken() - return &MemberExpr{Object: object, Property: p.curToken.Literal, position: object.Pos()} -} - -func (p *parser) parseIndexExpression(object Expression) Expression { - pos := p.curToken.Pos - p.nextToken() - index := p.parseExpression(lowestPrec) - if !p.expectPeek(tokenRBracket) { - return nil - } - return &IndexExpr{Object: object, Index: index, position: pos} -} - func (p *parser) curPrecedence() int { if prec, ok := precedences[p.curToken.Type]; ok { return prec diff --git a/vibes/parser_expressions.go b/vibes/parser_expressions.go new file mode 100644 index 0000000..b9f5cda --- /dev/null +++ b/vibes/parser_expressions.go @@ -0,0 +1,496 @@ +package vibes + +import ( + "fmt" + "strconv" + "strings" +) + +func (p *parser) parseExpression(precedence int) Expression { + prefix := p.prefixFns[p.curToken.Type] + if prefix == nil { + p.errorUnexpected(p.curToken) + return nil + } + + left := prefix() + if left == nil { + return nil + } + + for p.peekToken.Type != tokenEOF && precedence < p.peekPrecedence() { + infix := p.infixFns[p.peekToken.Type] + if infix == nil { + return left + } + p.nextToken() + left = infix(left) + if left == nil { + return nil + } + } + + return left +} + +func (p *parser) parseIdentifier() Expression { + return &Identifier{Name: p.curToken.Literal, position: p.curToken.Pos} +} + +func (p *parser) parseIntegerLiteral() Expression { + value, err := strconv.ParseInt(p.curToken.Literal, 10, 64) + if err != nil { + p.addParseError(p.curToken.Pos, "invalid integer literal") + return nil + } + return &IntegerLiteral{Value: value, position: p.curToken.Pos} +} + +func (p *parser) parseFloatLiteral() Expression { + value, err := strconv.ParseFloat(p.curToken.Literal, 64) + if err != nil { + p.addParseError(p.curToken.Pos, "invalid float literal") + return nil + } + return &FloatLiteral{Value: value, position: p.curToken.Pos} +} + +func (p *parser) parseStringLiteral() Expression { + return &StringLiteral{Value: p.curToken.Literal, position: p.curToken.Pos} +} + +func (p *parser) parseBooleanLiteral() Expression { + return &BoolLiteral{Value: p.curToken.Type == tokenTrue, position: p.curToken.Pos} +} + +func (p *parser) parseNilLiteral() Expression { + return &NilLiteral{position: p.curToken.Pos} +} + +func (p *parser) parseSymbolLiteral() Expression { + return &SymbolLiteral{Name: p.curToken.Literal, position: p.curToken.Pos} +} + +func (p *parser) parseIvarLiteral() Expression { + return &IvarExpr{Name: p.curToken.Literal, position: p.curToken.Pos} +} + +func (p *parser) parseClassVarLiteral() Expression { + return &ClassVarExpr{Name: p.curToken.Literal, position: p.curToken.Pos} +} + +func (p *parser) parseSelfLiteral() Expression { + return &Identifier{Name: "self", position: p.curToken.Pos} +} + +func (p *parser) parseYieldExpression() Expression { + pos := p.curToken.Pos + var args []Expression + if p.peekToken.Type == tokenLParen { + p.nextToken() + p.nextToken() + if p.curToken.Type != tokenRParen { + args = append(args, p.parseExpression(lowestPrec)) + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + args = append(args, p.parseExpression(lowestPrec)) + } + if !p.expectPeek(tokenRParen) { + return nil + } + } + } else if p.prefixFns[p.peekToken.Type] != nil { + p.nextToken() + args = append(args, p.parseExpression(lowestPrec)) + } + return &YieldExpr{Args: args, position: pos} +} + +func (p *parser) parseCaseExpression() Expression { + pos := p.curToken.Pos + p.nextToken() + target := p.parseExpression(lowestPrec) + if target == nil { + return nil + } + + p.nextToken() + clauses := []CaseWhenClause{} + for p.curToken.Type == tokenWhen { + p.nextToken() + values := []Expression{} + first := p.parseExpression(lowestPrec) + if first == nil { + return nil + } + values = append(values, first) + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + value := p.parseExpression(lowestPrec) + if value == nil { + return nil + } + values = append(values, value) + } + + p.nextToken() + result := p.parseExpressionWithBlock() + if result == nil { + return nil + } + clauses = append(clauses, CaseWhenClause{Values: values, Result: result}) + p.nextToken() + } + + if len(clauses) == 0 { + p.errorExpected(p.curToken, "when") + return nil + } + + var elseExpr Expression + if p.curToken.Type == tokenElse { + p.nextToken() + elseExpr = p.parseExpressionWithBlock() + if elseExpr == nil { + return nil + } + p.nextToken() + } + + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + return nil + } + + return &CaseExpr{Target: target, Clauses: clauses, ElseExpr: elseExpr, position: pos} +} + +func (p *parser) parseGroupedExpression() Expression { + p.nextToken() + expr := p.parseExpression(lowestPrec) + if !p.expectPeek(tokenRParen) { + return nil + } + return expr +} + +func (p *parser) parseArrayLiteral() Expression { + pos := p.curToken.Pos + elements := []Expression{} + + if p.peekToken.Type == tokenRBracket { + p.nextToken() + return &ArrayLiteral{Elements: elements, position: pos} + } + + p.nextToken() + elements = append(elements, p.parseExpression(lowestPrec)) + + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + elements = append(elements, p.parseExpression(lowestPrec)) + } + + if !p.expectPeek(tokenRBracket) { + return nil + } + + return &ArrayLiteral{Elements: elements, position: pos} +} + +func (p *parser) parseHashLiteral() Expression { + pos := p.curToken.Pos + pairs := []HashPair{} + + if p.peekToken.Type == tokenRBrace { + p.nextToken() + return &HashLiteral{Pairs: pairs, position: pos} + } + + p.nextToken() + if pair := p.parseHashPair(); pair.Key != nil { + pairs = append(pairs, pair) + } + + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + if pair := p.parseHashPair(); pair.Key != nil { + pairs = append(pairs, pair) + } + } + + if !p.expectPeek(tokenRBrace) { + return nil + } + + return &HashLiteral{Pairs: pairs, position: pos} +} + +func (p *parser) parseHashPair() HashPair { + if !isLabelNameToken(p.curToken.Type) || p.peekToken.Type != tokenColon { + p.addParseError(p.curToken.Pos, "invalid hash pair: expected symbol-style key like name:") + return HashPair{} + } + + key := &SymbolLiteral{Name: p.curToken.Literal, position: p.curToken.Pos} + p.nextToken() + p.nextToken() + if p.curToken.Type == tokenComma || p.curToken.Type == tokenRBrace { + p.addParseError(p.curToken.Pos, fmt.Sprintf("missing value for hash key %s", key.Name)) + return HashPair{} + } + + value := p.parseExpression(lowestPrec) + if value == nil { + return HashPair{} + } + return HashPair{Key: key, Value: value} +} + +func (p *parser) parsePrefixExpression() Expression { + pos := p.curToken.Pos + operator := p.curToken.Type + p.nextToken() + right := p.parseExpression(precPrefix) + return &UnaryExpr{Operator: operator, Right: right, position: pos} +} + +func (p *parser) parseInfixExpression(left Expression) Expression { + pos := p.curToken.Pos + operator := p.curToken.Type + precedence := p.curPrecedence() + p.nextToken() + right := p.parseExpression(precedence) + return &BinaryExpr{Left: left, Operator: operator, Right: right, position: pos} +} + +func (p *parser) parseRangeExpression(left Expression) Expression { + pos := p.curToken.Pos + precedence := p.curPrecedence() + p.nextToken() + right := p.parseExpression(precedence) + return &RangeExpr{Start: left, End: right, position: pos} +} + +func (p *parser) parseCallExpression(function Expression) Expression { + if function == nil { + return nil + } + expr := &CallExpr{Callee: function, position: function.Pos()} + args := []Expression{} + kwargs := []KeywordArg{} + + if p.peekToken.Type == tokenRParen { + p.nextToken() + expr.Args = args + expr.KwArgs = kwargs + return expr + } + + p.nextToken() + p.parseCallArgument(&args, &kwargs) + + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + p.parseCallArgument(&args, &kwargs) + } + + if !p.expectPeek(tokenRParen) { + return nil + } + + expr.Args = args + expr.KwArgs = kwargs + if p.peekToken.Type == tokenDo { + p.nextToken() + expr.Block = p.parseBlockLiteral() + } + return expr +} + +func (p *parser) parseCallArgument(args *[]Expression, kwargs *[]KeywordArg) { + if isLabelNameToken(p.curToken.Type) && p.peekToken.Type == tokenColon { + name := p.curToken.Literal + p.nextToken() + p.nextToken() + if p.curToken.Type == tokenComma || p.curToken.Type == tokenRParen { + p.addParseError(p.curToken.Pos, fmt.Sprintf("missing value for keyword argument %s", name)) + return + } + value := p.parseExpression(lowestPrec) + if value == nil { + return + } + *kwargs = append(*kwargs, KeywordArg{Name: name, Value: value}) + return + } + + expr := p.parseExpression(lowestPrec) + if expr != nil { + *args = append(*args, expr) + } +} + +func isLabelNameToken(tt TokenType) bool { + switch tt { + case tokenIdent, + tokenDef, tokenClass, tokenSelf, tokenPrivate, tokenProperty, tokenGetter, tokenSetter, + tokenEnd, tokenReturn, tokenYield, tokenDo, tokenFor, tokenWhile, tokenUntil, + tokenBreak, tokenNext, tokenIn, tokenIf, tokenCase, tokenWhen, tokenElsif, tokenElse, + tokenTrue, tokenFalse, tokenNil: + return true + default: + return false + } +} + +func (p *parser) parseBlockLiteral() *BlockLiteral { + pos := p.curToken.Pos + params := []Param{} + + p.nextToken() + if p.curToken.Type == tokenPipe { + var ok bool + params, ok = p.parseBlockParameters() + if !ok { + return nil + } + p.nextToken() + } + + body := p.parseBlock(tokenEnd) + if p.curToken.Type != tokenEnd { + p.errorExpected(p.curToken, "end") + } + + return &BlockLiteral{Params: params, Body: body, position: pos} +} + +func (p *parser) parseBlockParameters() ([]Param, bool) { + params := []Param{} + p.nextToken() + if p.curToken.Type == tokenPipe { + return params, true + } + + param, ok := p.parseBlockParameter() + if !ok { + return nil, false + } + params = append(params, param) + + for p.peekToken.Type == tokenComma { + p.nextToken() + p.nextToken() + if p.curToken.Type == tokenPipe { + p.addParseError(p.curToken.Pos, "trailing comma in block parameter list") + return nil, false + } + param, ok := p.parseBlockParameter() + if !ok { + return nil, false + } + params = append(params, param) + } + + if !p.expectPeek(tokenPipe) { + return nil, false + } + + return params, true +} + +func (p *parser) parseBlockParameter() (Param, bool) { + if p.curToken.Type != tokenIdent { + p.errorExpected(p.curToken, "block parameter") + return Param{}, false + } + param := Param{Name: p.curToken.Literal} + if p.peekToken.Type == tokenColon { + p.nextToken() + p.nextToken() + param.Type = p.parseBlockParamType() + if param.Type == nil { + return Param{}, false + } + } + return param, true +} + +func (p *parser) parseBlockParamType() *TypeExpr { + first := p.parseTypeAtom() + if first == nil { + return nil + } + + union := []*TypeExpr{first} + for p.peekToken.Type == tokenPipe && p.blockParamUnionContinues() { + p.nextToken() + p.nextToken() + next := p.parseTypeAtom() + if next == nil { + return nil + } + union = append(union, next) + } + + if len(union) == 1 { + return first + } + + names := make([]string, len(union)) + for i, option := range union { + names[i] = formatTypeExpr(option) + } + return &TypeExpr{ + Name: strings.Join(names, " | "), + Kind: TypeUnion, + Union: union, + position: first.position, + } +} + +func (p *parser) blockParamUnionContinues() bool { + if p.peekToken.Type != tokenPipe { + return false + } + + savedLexer := *p.l + savedCur := p.curToken + savedPeek := p.peekToken + savedErrors := len(p.errors) + + p.nextToken() + p.nextToken() + atom := p.parseTypeAtom() + ok := atom != nil && (p.peekToken.Type == tokenComma || p.peekToken.Type == tokenPipe) + + p.l = &savedLexer + p.curToken = savedCur + p.peekToken = savedPeek + p.errors = p.errors[:savedErrors] + return ok +} + +func (p *parser) parseMemberExpression(object Expression) Expression { + if object == nil { + return nil + } + p.nextToken() + return &MemberExpr{Object: object, Property: p.curToken.Literal, position: object.Pos()} +} + +func (p *parser) parseIndexExpression(object Expression) Expression { + pos := p.curToken.Pos + p.nextToken() + index := p.parseExpression(lowestPrec) + if !p.expectPeek(tokenRBracket) { + return nil + } + return &IndexExpr{Object: object, Index: index, position: pos} +} From 8e54bad983b94d2e144778ea482d308dd748814f Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:13:06 -0500 Subject: [PATCH 10/99] extract control flow evaluators from execution core --- vibes/execution.go | 323 ------------------------------------ vibes/execution_control.go | 329 +++++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+), 323 deletions(-) create mode 100644 vibes/execution_control.go diff --git a/vibes/execution.go b/vibes/execution.go index 348e809..9bcf38e 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -1194,326 +1194,3 @@ func (exec *Execution) evalYield(expr *YieldExpr, env *Env) (Value, error) { } return exec.CallBlock(block, args) } - -func (exec *Execution) evalRangeExpr(expr *RangeExpr, env *Env) (Value, error) { - startVal, err := exec.evalExpression(expr.Start, env) - if err != nil { - return NewNil(), err - } - endVal, err := exec.evalExpression(expr.End, env) - if err != nil { - return NewNil(), err - } - start, err := valueToInt64(startVal) - if err != nil { - return NewNil(), exec.errorAt(expr.Start.Pos(), "%s", err.Error()) - } - end, err := valueToInt64(endVal) - if err != nil { - return NewNil(), exec.errorAt(expr.End.Pos(), "%s", err.Error()) - } - return NewRange(Range{Start: start, End: end}), nil -} - -func (exec *Execution) evalCaseExpr(expr *CaseExpr, env *Env) (Value, error) { - target, err := exec.evalExpression(expr.Target, env) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(target); err != nil { - return NewNil(), err - } - - for _, clause := range expr.Clauses { - matched := false - for _, candidateExpr := range clause.Values { - candidate, err := exec.evalExpression(candidateExpr, env) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(candidate); err != nil { - return NewNil(), err - } - if target.Equal(candidate) { - matched = true - break - } - } - if !matched { - continue - } - result, err := exec.evalExpressionWithAuto(clause.Result, env, true) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(result); err != nil { - return NewNil(), err - } - return result, nil - } - - if expr.ElseExpr != nil { - result, err := exec.evalExpressionWithAuto(expr.ElseExpr, env, true) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(result); err != nil { - return NewNil(), err - } - return result, nil - } - - return NewNil(), nil -} - -func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, error) { - exec.loopDepth++ - defer func() { - exec.loopDepth-- - }() - - iterable, err := exec.evalExpression(stmt.Iterable, env) - if err != nil { - return NewNil(), false, err - } - if err := exec.checkMemoryWith(iterable); err != nil { - return NewNil(), false, err - } - last := NewNil() - - switch iterable.Kind() { - case KindArray: - arr := iterable.Array() - for _, item := range arr { - env.Assign(stmt.Iterator, item) - val, returned, err := exec.evalStatements(stmt.Body, env) - if err != nil { - if errors.Is(err, errLoopBreak) { - return last, false, nil - } - if errors.Is(err, errLoopNext) { - continue - } - return NewNil(), false, err - } - if returned { - return val, true, nil - } - last = val - } - case KindRange: - r := iterable.Range() - if r.Start <= r.End { - for i := r.Start; i <= r.End; i++ { - env.Assign(stmt.Iterator, NewInt(i)) - val, returned, err := exec.evalStatements(stmt.Body, env) - if err != nil { - if errors.Is(err, errLoopBreak) { - return last, false, nil - } - if errors.Is(err, errLoopNext) { - continue - } - return NewNil(), false, err - } - if returned { - return val, true, nil - } - last = val - } - } else { - for i := r.Start; i >= r.End; i-- { - env.Assign(stmt.Iterator, NewInt(i)) - val, returned, err := exec.evalStatements(stmt.Body, env) - if err != nil { - if errors.Is(err, errLoopBreak) { - return last, false, nil - } - if errors.Is(err, errLoopNext) { - continue - } - return NewNil(), false, err - } - if returned { - return val, true, nil - } - last = val - } - } - default: - return NewNil(), false, exec.errorAt(stmt.Pos(), "cannot iterate over %s", iterable.Kind()) - } - - return last, false, nil -} - -func (exec *Execution) evalWhileStatement(stmt *WhileStmt, env *Env) (Value, bool, error) { - exec.loopDepth++ - defer func() { - exec.loopDepth-- - }() - - last := NewNil() - for { - if err := exec.step(); err != nil { - return NewNil(), false, exec.wrapError(err, stmt.Pos()) - } - condition, err := exec.evalExpression(stmt.Condition, env) - if err != nil { - return NewNil(), false, err - } - if err := exec.checkMemoryWith(condition); err != nil { - return NewNil(), false, err - } - if !condition.Truthy() { - return last, false, nil - } - val, returned, err := exec.evalStatements(stmt.Body, env) - if err != nil { - if errors.Is(err, errLoopBreak) { - return last, false, nil - } - if errors.Is(err, errLoopNext) { - continue - } - return NewNil(), false, err - } - if returned { - return val, true, nil - } - last = val - } -} - -func (exec *Execution) evalUntilStatement(stmt *UntilStmt, env *Env) (Value, bool, error) { - exec.loopDepth++ - defer func() { - exec.loopDepth-- - }() - - last := NewNil() - for { - if err := exec.step(); err != nil { - return NewNil(), false, exec.wrapError(err, stmt.Pos()) - } - condition, err := exec.evalExpression(stmt.Condition, env) - if err != nil { - return NewNil(), false, err - } - if err := exec.checkMemoryWith(condition); err != nil { - return NewNil(), false, err - } - if condition.Truthy() { - return last, false, nil - } - val, returned, err := exec.evalStatements(stmt.Body, env) - if err != nil { - if errors.Is(err, errLoopBreak) { - return last, false, nil - } - if errors.Is(err, errLoopNext) { - continue - } - return NewNil(), false, err - } - if returned { - return val, true, nil - } - last = val - } -} - -func (exec *Execution) evalRaiseStatement(stmt *RaiseStmt, env *Env) (Value, bool, error) { - if stmt.Value != nil { - val, err := exec.evalExpression(stmt.Value, env) - if err != nil { - return NewNil(), false, err - } - return NewNil(), false, exec.errorAt(stmt.Pos(), "%s", val.String()) - } - - err := exec.currentRescuedError() - if err == nil { - return NewNil(), false, exec.errorAt(stmt.Pos(), "raise used outside of rescue") - } - return NewNil(), false, err -} - -func (exec *Execution) evalTryStatement(stmt *TryStmt, env *Env) (Value, bool, error) { - val, returned, err := exec.evalStatements(stmt.Body, env) - - if err != nil && !isLoopControlSignal(err) && !isHostControlSignal(err) && len(stmt.Rescue) > 0 && runtimeErrorMatchesRescueType(err, stmt.RescueTy) { - exec.pushRescuedError(err) - rescueVal, rescueReturned, rescueErr := exec.evalStatements(stmt.Rescue, env) - exec.popRescuedError() - if rescueErr != nil { - val = NewNil() - returned = false - err = rescueErr - } else { - val = rescueVal - returned = rescueReturned - err = nil - } - } - - if len(stmt.Ensure) > 0 { - ensureVal, ensureReturned, ensureErr := exec.evalStatements(stmt.Ensure, env) - if ensureErr != nil { - return NewNil(), false, ensureErr - } - if ensureReturned { - return ensureVal, true, nil - } - } - - if err != nil { - return NewNil(), false, err - } - return val, returned, nil -} - -func isLoopControlSignal(err error) bool { - return errors.Is(err, errLoopBreak) || errors.Is(err, errLoopNext) -} - -func isHostControlSignal(err error) bool { - return errors.Is(err, context.Canceled) || - errors.Is(err, context.DeadlineExceeded) || - errors.Is(err, errStepQuotaExceeded) || - errors.Is(err, errMemoryQuotaExceeded) -} - -func runtimeErrorMatchesRescueType(err error, rescueTy *TypeExpr) bool { - var runtimeErr *RuntimeError - if !errors.As(err, &runtimeErr) { - return false - } - if rescueTy == nil { - return true - } - errKind := classifyRuntimeErrorType(err) - return rescueTypeMatchesErrorKind(rescueTy, errKind) -} - -func rescueTypeMatchesErrorKind(ty *TypeExpr, errKind string) bool { - if ty == nil { - return false - } - if ty.Kind == TypeUnion { - for _, option := range ty.Union { - if rescueTypeMatchesErrorKind(option, errKind) { - return true - } - } - return false - } - canonical, ok := canonicalRuntimeErrorType(ty.Name) - if !ok { - return false - } - if canonical == runtimeErrorTypeBase { - return true - } - return canonical == errKind -} diff --git a/vibes/execution_control.go b/vibes/execution_control.go new file mode 100644 index 0000000..9233e64 --- /dev/null +++ b/vibes/execution_control.go @@ -0,0 +1,329 @@ +package vibes + +import ( + "context" + "errors" +) + +func (exec *Execution) evalRangeExpr(expr *RangeExpr, env *Env) (Value, error) { + startVal, err := exec.evalExpression(expr.Start, env) + if err != nil { + return NewNil(), err + } + endVal, err := exec.evalExpression(expr.End, env) + if err != nil { + return NewNil(), err + } + start, err := valueToInt64(startVal) + if err != nil { + return NewNil(), exec.errorAt(expr.Start.Pos(), "%s", err.Error()) + } + end, err := valueToInt64(endVal) + if err != nil { + return NewNil(), exec.errorAt(expr.End.Pos(), "%s", err.Error()) + } + return NewRange(Range{Start: start, End: end}), nil +} + +func (exec *Execution) evalCaseExpr(expr *CaseExpr, env *Env) (Value, error) { + target, err := exec.evalExpression(expr.Target, env) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(target); err != nil { + return NewNil(), err + } + + for _, clause := range expr.Clauses { + matched := false + for _, candidateExpr := range clause.Values { + candidate, err := exec.evalExpression(candidateExpr, env) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(candidate); err != nil { + return NewNil(), err + } + if target.Equal(candidate) { + matched = true + break + } + } + if !matched { + continue + } + result, err := exec.evalExpressionWithAuto(clause.Result, env, true) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(result); err != nil { + return NewNil(), err + } + return result, nil + } + + if expr.ElseExpr != nil { + result, err := exec.evalExpressionWithAuto(expr.ElseExpr, env, true) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(result); err != nil { + return NewNil(), err + } + return result, nil + } + + return NewNil(), nil +} + +func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, error) { + exec.loopDepth++ + defer func() { + exec.loopDepth-- + }() + + iterable, err := exec.evalExpression(stmt.Iterable, env) + if err != nil { + return NewNil(), false, err + } + if err := exec.checkMemoryWith(iterable); err != nil { + return NewNil(), false, err + } + last := NewNil() + + switch iterable.Kind() { + case KindArray: + arr := iterable.Array() + for _, item := range arr { + env.Assign(stmt.Iterator, item) + val, returned, err := exec.evalStatements(stmt.Body, env) + if err != nil { + if errors.Is(err, errLoopBreak) { + return last, false, nil + } + if errors.Is(err, errLoopNext) { + continue + } + return NewNil(), false, err + } + if returned { + return val, true, nil + } + last = val + } + case KindRange: + r := iterable.Range() + if r.Start <= r.End { + for i := r.Start; i <= r.End; i++ { + env.Assign(stmt.Iterator, NewInt(i)) + val, returned, err := exec.evalStatements(stmt.Body, env) + if err != nil { + if errors.Is(err, errLoopBreak) { + return last, false, nil + } + if errors.Is(err, errLoopNext) { + continue + } + return NewNil(), false, err + } + if returned { + return val, true, nil + } + last = val + } + } else { + for i := r.Start; i >= r.End; i-- { + env.Assign(stmt.Iterator, NewInt(i)) + val, returned, err := exec.evalStatements(stmt.Body, env) + if err != nil { + if errors.Is(err, errLoopBreak) { + return last, false, nil + } + if errors.Is(err, errLoopNext) { + continue + } + return NewNil(), false, err + } + if returned { + return val, true, nil + } + last = val + } + } + default: + return NewNil(), false, exec.errorAt(stmt.Pos(), "cannot iterate over %s", iterable.Kind()) + } + + return last, false, nil +} + +func (exec *Execution) evalWhileStatement(stmt *WhileStmt, env *Env) (Value, bool, error) { + exec.loopDepth++ + defer func() { + exec.loopDepth-- + }() + + last := NewNil() + for { + if err := exec.step(); err != nil { + return NewNil(), false, exec.wrapError(err, stmt.Pos()) + } + condition, err := exec.evalExpression(stmt.Condition, env) + if err != nil { + return NewNil(), false, err + } + if err := exec.checkMemoryWith(condition); err != nil { + return NewNil(), false, err + } + if !condition.Truthy() { + return last, false, nil + } + val, returned, err := exec.evalStatements(stmt.Body, env) + if err != nil { + if errors.Is(err, errLoopBreak) { + return last, false, nil + } + if errors.Is(err, errLoopNext) { + continue + } + return NewNil(), false, err + } + if returned { + return val, true, nil + } + last = val + } +} + +func (exec *Execution) evalUntilStatement(stmt *UntilStmt, env *Env) (Value, bool, error) { + exec.loopDepth++ + defer func() { + exec.loopDepth-- + }() + + last := NewNil() + for { + if err := exec.step(); err != nil { + return NewNil(), false, exec.wrapError(err, stmt.Pos()) + } + condition, err := exec.evalExpression(stmt.Condition, env) + if err != nil { + return NewNil(), false, err + } + if err := exec.checkMemoryWith(condition); err != nil { + return NewNil(), false, err + } + if condition.Truthy() { + return last, false, nil + } + val, returned, err := exec.evalStatements(stmt.Body, env) + if err != nil { + if errors.Is(err, errLoopBreak) { + return last, false, nil + } + if errors.Is(err, errLoopNext) { + continue + } + return NewNil(), false, err + } + if returned { + return val, true, nil + } + last = val + } +} + +func (exec *Execution) evalRaiseStatement(stmt *RaiseStmt, env *Env) (Value, bool, error) { + if stmt.Value != nil { + val, err := exec.evalExpression(stmt.Value, env) + if err != nil { + return NewNil(), false, err + } + return NewNil(), false, exec.errorAt(stmt.Pos(), "%s", val.String()) + } + + err := exec.currentRescuedError() + if err == nil { + return NewNil(), false, exec.errorAt(stmt.Pos(), "raise used outside of rescue") + } + return NewNil(), false, err +} + +func (exec *Execution) evalTryStatement(stmt *TryStmt, env *Env) (Value, bool, error) { + val, returned, err := exec.evalStatements(stmt.Body, env) + + if err != nil && !isLoopControlSignal(err) && !isHostControlSignal(err) && len(stmt.Rescue) > 0 && runtimeErrorMatchesRescueType(err, stmt.RescueTy) { + exec.pushRescuedError(err) + rescueVal, rescueReturned, rescueErr := exec.evalStatements(stmt.Rescue, env) + exec.popRescuedError() + if rescueErr != nil { + val = NewNil() + returned = false + err = rescueErr + } else { + val = rescueVal + returned = rescueReturned + err = nil + } + } + + if len(stmt.Ensure) > 0 { + ensureVal, ensureReturned, ensureErr := exec.evalStatements(stmt.Ensure, env) + if ensureErr != nil { + return NewNil(), false, ensureErr + } + if ensureReturned { + return ensureVal, true, nil + } + } + + if err != nil { + return NewNil(), false, err + } + return val, returned, nil +} + +func isLoopControlSignal(err error) bool { + return errors.Is(err, errLoopBreak) || errors.Is(err, errLoopNext) +} + +func isHostControlSignal(err error) bool { + return errors.Is(err, context.Canceled) || + errors.Is(err, context.DeadlineExceeded) || + errors.Is(err, errStepQuotaExceeded) || + errors.Is(err, errMemoryQuotaExceeded) +} + +func runtimeErrorMatchesRescueType(err error, rescueTy *TypeExpr) bool { + var runtimeErr *RuntimeError + if !errors.As(err, &runtimeErr) { + return false + } + if rescueTy == nil { + return true + } + errKind := classifyRuntimeErrorType(err) + return rescueTypeMatchesErrorKind(rescueTy, errKind) +} + +func rescueTypeMatchesErrorKind(ty *TypeExpr, errKind string) bool { + if ty == nil { + return false + } + if ty.Kind == TypeUnion { + for _, option := range ty.Union { + if rescueTypeMatchesErrorKind(option, errKind) { + return true + } + } + return false + } + canonical, ok := canonicalRuntimeErrorType(ty.Name) + if !ok { + return false + } + if canonical == runtimeErrorTypeBase { + return true + } + return canonical == errKind +} From 3e6226b8cf432ef0163b0ceaddd06d1328a77b47 Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:13:14 -0500 Subject: [PATCH 11/99] split execution member handlers by value domain --- vibes/execution_members.go | 2086 --------------------------- vibes/execution_members_array.go | 755 ++++++++++ vibes/execution_members_duration.go | 103 ++ vibes/execution_members_hash.go | 411 ++++++ vibes/execution_members_string.go | 839 +++++++++++ 5 files changed, 2108 insertions(+), 2086 deletions(-) create mode 100644 vibes/execution_members_array.go create mode 100644 vibes/execution_members_duration.go create mode 100644 vibes/execution_members_hash.go create mode 100644 vibes/execution_members_string.go diff --git a/vibes/execution_members.go b/vibes/execution_members.go index a2b255c..f732673 100644 --- a/vibes/execution_members.go +++ b/vibes/execution_members.go @@ -2,15 +2,7 @@ package vibes import ( "fmt" - "maps" "math" - "reflect" - "regexp" - "slices" - "sort" - "strings" - "time" - "unicode" ) func (exec *Execution) getMember(obj Value, property string, pos Position) (Value, error) { @@ -249,2081 +241,3 @@ func moneyMember(m Money, property string) (Value, error) { return NewNil(), fmt.Errorf("unknown money member %s", property) } } - -func hashMember(obj Value, property string) (Value, error) { - switch property { - case "size": - return NewAutoBuiltin("hash.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.size does not take arguments") - } - return NewInt(int64(len(receiver.Hash()))), nil - }), nil - case "length": - return NewAutoBuiltin("hash.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.length does not take arguments") - } - return NewInt(int64(len(receiver.Hash()))), nil - }), nil - case "empty?": - return NewAutoBuiltin("hash.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.empty? does not take arguments") - } - return NewBool(len(receiver.Hash()) == 0), nil - }), nil - case "key?", "has_key?", "include?": - name := property - return NewAutoBuiltin("hash."+name, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("hash.%s expects exactly one key", name) - } - key, err := valueToHashKey(args[0]) - if err != nil { - return NewNil(), fmt.Errorf("hash.%s key must be symbol or string", name) - } - _, ok := receiver.Hash()[key] - return NewBool(ok), nil - }), nil - case "keys": - return NewAutoBuiltin("hash.keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.keys does not take arguments") - } - keys := sortedHashKeys(receiver.Hash()) - values := make([]Value, len(keys)) - for i, k := range keys { - values[i] = NewSymbol(k) - } - return NewArray(values), nil - }), nil - case "values": - return NewAutoBuiltin("hash.values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.values does not take arguments") - } - entries := receiver.Hash() - keys := sortedHashKeys(entries) - values := make([]Value, len(keys)) - for i, k := range keys { - values[i] = entries[k] - } - return NewArray(values), nil - }), nil - case "fetch": - return NewAutoBuiltin("hash.fetch", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("hash.fetch expects key and optional default") - } - key, err := valueToHashKey(args[0]) - if err != nil { - return NewNil(), fmt.Errorf("hash.fetch key must be symbol or string") - } - if value, ok := receiver.Hash()[key]; ok { - return value, nil - } - if len(args) == 2 { - return args[1], nil - } - return NewNil(), nil - }), nil - case "dig": - return NewAutoBuiltin("hash.dig", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) == 0 { - return NewNil(), fmt.Errorf("hash.dig expects at least one key") - } - current := receiver - for _, arg := range args { - key, err := valueToHashKey(arg) - if err != nil { - return NewNil(), fmt.Errorf("hash.dig path keys must be symbol or string") - } - if current.Kind() != KindHash && current.Kind() != KindObject { - return NewNil(), nil - } - next, ok := current.Hash()[key] - if !ok { - return NewNil(), nil - } - current = next - } - return current, nil - }), nil - case "each": - return NewAutoBuiltin("hash.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.each does not take arguments") - } - if err := ensureBlock(block, "hash.each"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - for _, key := range sortedHashKeys(entries) { - if _, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "each_key": - return NewAutoBuiltin("hash.each_key", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.each_key does not take arguments") - } - if err := ensureBlock(block, "hash.each_key"); err != nil { - return NewNil(), err - } - for _, key := range sortedHashKeys(receiver.Hash()) { - if _, err := exec.CallBlock(block, []Value{NewSymbol(key)}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "each_value": - return NewAutoBuiltin("hash.each_value", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.each_value does not take arguments") - } - if err := ensureBlock(block, "hash.each_value"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - for _, key := range sortedHashKeys(entries) { - if _, err := exec.CallBlock(block, []Value{entries[key]}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "merge": - return NewBuiltin("hash.merge", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { - return NewNil(), fmt.Errorf("hash.merge expects a single hash argument") - } - base := receiver.Hash() - addition := args[0].Hash() - out := make(map[string]Value, len(base)+len(addition)) - maps.Copy(out, base) - maps.Copy(out, addition) - return NewHash(out), nil - }), nil - case "slice": - return NewBuiltin("hash.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - entries := receiver.Hash() - out := make(map[string]Value, len(args)) - for _, arg := range args { - key, err := valueToHashKey(arg) - if err != nil { - return NewNil(), fmt.Errorf("hash.slice keys must be symbol or string") - } - if value, ok := entries[key]; ok { - out[key] = value - } - } - return NewHash(out), nil - }), nil - case "except": - return NewBuiltin("hash.except", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - excluded := make(map[string]struct{}, len(args)) - for _, arg := range args { - key, err := valueToHashKey(arg) - if err != nil { - return NewNil(), fmt.Errorf("hash.except keys must be symbol or string") - } - excluded[key] = struct{}{} - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for key, value := range entries { - if _, skip := excluded[key]; skip { - continue - } - out[key] = value - } - return NewHash(out), nil - }), nil - case "select": - return NewAutoBuiltin("hash.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.select does not take arguments") - } - if err := ensureBlock(block, "hash.select"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - include, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) - if err != nil { - return NewNil(), err - } - if include.Truthy() { - out[key] = entries[key] - } - } - return NewHash(out), nil - }), nil - case "reject": - return NewAutoBuiltin("hash.reject", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.reject does not take arguments") - } - if err := ensureBlock(block, "hash.reject"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - exclude, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) - if err != nil { - return NewNil(), err - } - if !exclude.Truthy() { - out[key] = entries[key] - } - } - return NewHash(out), nil - }), nil - case "transform_keys": - return NewAutoBuiltin("hash.transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.transform_keys does not take arguments") - } - if err := ensureBlock(block, "hash.transform_keys"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - nextKey, err := exec.CallBlock(block, []Value{NewSymbol(key)}) - if err != nil { - return NewNil(), err - } - resolved, err := valueToHashKey(nextKey) - if err != nil { - return NewNil(), fmt.Errorf("hash.transform_keys block must return symbol or string") - } - out[resolved] = entries[key] - } - return NewHash(out), nil - }), nil - case "deep_transform_keys": - return NewAutoBuiltin("hash.deep_transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.deep_transform_keys does not take arguments") - } - if err := ensureBlock(block, "hash.deep_transform_keys"); err != nil { - return NewNil(), err - } - return deepTransformKeys(exec, receiver, block) - }), nil - case "remap_keys": - return NewBuiltin("hash.remap_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { - return NewNil(), fmt.Errorf("hash.remap_keys expects a key mapping hash") - } - entries := receiver.Hash() - mapping := args[0].Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - value := entries[key] - if mapped, ok := mapping[key]; ok { - nextKey, err := valueToHashKey(mapped) - if err != nil { - return NewNil(), fmt.Errorf("hash.remap_keys mapping values must be symbol or string") - } - out[nextKey] = value - continue - } - out[key] = value - } - return NewHash(out), nil - }), nil - case "transform_values": - return NewAutoBuiltin("hash.transform_values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.transform_values does not take arguments") - } - if err := ensureBlock(block, "hash.transform_values"); err != nil { - return NewNil(), err - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - nextValue, err := exec.CallBlock(block, []Value{entries[key]}) - if err != nil { - return NewNil(), err - } - out[key] = nextValue - } - return NewHash(out), nil - }), nil - case "compact": - return NewAutoBuiltin("hash.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("hash.compact does not take arguments") - } - entries := receiver.Hash() - out := make(map[string]Value, len(entries)) - for k, v := range entries { - if v.Kind() != KindNil { - out[k] = v - } - } - return NewHash(out), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown hash method %s", property) - } -} - -func sortedHashKeys(entries map[string]Value) []string { - keys := make([]string, 0, len(entries)) - for key := range entries { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -func deepTransformKeys(exec *Execution, value Value, block Value) (Value, error) { - return deepTransformKeysWithState(exec, value, block, &deepTransformState{ - seenHashes: make(map[uintptr]struct{}), - seenArrays: make(map[uintptr]struct{}), - }) -} - -type deepTransformState struct { - seenHashes map[uintptr]struct{} - seenArrays map[uintptr]struct{} -} - -func deepTransformKeysWithState(exec *Execution, value Value, block Value, state *deepTransformState) (Value, error) { - switch value.Kind() { - case KindHash, KindObject: - entries := value.Hash() - id := reflect.ValueOf(entries).Pointer() - if id != 0 { - if _, seen := state.seenHashes[id]; seen { - return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") - } - state.seenHashes[id] = struct{}{} - defer delete(state.seenHashes, id) - } - out := make(map[string]Value, len(entries)) - for _, key := range sortedHashKeys(entries) { - nextKeyValue, err := exec.CallBlock(block, []Value{NewSymbol(key)}) - if err != nil { - return NewNil(), err - } - nextKey, err := valueToHashKey(nextKeyValue) - if err != nil { - return NewNil(), fmt.Errorf("hash.deep_transform_keys block must return symbol or string") - } - nextValue, err := deepTransformKeysWithState(exec, entries[key], block, state) - if err != nil { - return NewNil(), err - } - out[nextKey] = nextValue - } - return NewHash(out), nil - case KindArray: - items := value.Array() - id := reflect.ValueOf(items).Pointer() - if id != 0 { - if _, seen := state.seenArrays[id]; seen { - return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") - } - state.seenArrays[id] = struct{}{} - defer delete(state.seenArrays, id) - } - out := make([]Value, len(items)) - for i, item := range items { - nextValue, err := deepTransformKeysWithState(exec, item, block, state) - if err != nil { - return NewNil(), err - } - out[i] = nextValue - } - return NewArray(out), nil - default: - return value, nil - } -} - -func chompDefault(text string) string { - if strings.HasSuffix(text, "\r\n") { - return text[:len(text)-2] - } - if strings.HasSuffix(text, "\n") || strings.HasSuffix(text, "\r") { - return text[:len(text)-1] - } - return text -} - -func stringRuneIndex(text, needle string, offset int) int { - hayRunes := []rune(text) - needleRunes := []rune(needle) - if offset < 0 || offset > len(hayRunes) { - return -1 - } - if len(needleRunes) == 0 { - return offset - } - limit := len(hayRunes) - len(needleRunes) - if limit < offset { - return -1 - } - for i := offset; i <= limit; i++ { - match := true - for j := range len(needleRunes) { - if hayRunes[i+j] != needleRunes[j] { - match = false - break - } - } - if match { - return i - } - } - return -1 -} - -func stringRuneRIndex(text, needle string, offset int) int { - hayRunes := []rune(text) - needleRunes := []rune(needle) - if offset < 0 { - return -1 - } - if offset > len(hayRunes) { - offset = len(hayRunes) - } - if len(needleRunes) == 0 { - return offset - } - if len(needleRunes) > len(hayRunes) { - return -1 - } - start := offset - maxStart := len(hayRunes) - len(needleRunes) - if start > maxStart { - start = maxStart - } - for i := start; i >= 0; i-- { - match := true - for j := range len(needleRunes) { - if hayRunes[i+j] != needleRunes[j] { - match = false - break - } - } - if match { - return i - } - } - return -1 -} - -func stringRuneSlice(text string, start, length int) (string, bool) { - runes := []rune(text) - if start < 0 || start >= len(runes) { - return "", false - } - if length < 0 { - return "", false - } - remaining := len(runes) - start - if length >= remaining { - return string(runes[start:]), true - } - end := start + length - return string(runes[start:end]), true -} - -func stringCapitalize(text string) string { - runes := []rune(text) - if len(runes) == 0 { - return "" - } - runes[0] = unicode.ToUpper(runes[0]) - for i := 1; i < len(runes); i++ { - runes[i] = unicode.ToLower(runes[i]) - } - return string(runes) -} - -func stringSwapCase(text string) string { - runes := []rune(text) - for i, r := range runes { - if unicode.IsUpper(r) { - runes[i] = unicode.ToLower(r) - continue - } - if unicode.IsLower(r) { - runes[i] = unicode.ToUpper(r) - } - } - return string(runes) -} - -func stringReverse(text string) string { - runes := []rune(text) - for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { - runes[i], runes[j] = runes[j], runes[i] - } - return string(runes) -} - -func stringRegexOption(method string, kwargs map[string]Value) (bool, error) { - if len(kwargs) == 0 { - return false, nil - } - regexVal, ok := kwargs["regex"] - if !ok || len(kwargs) > 1 { - return false, fmt.Errorf("string.%s supports only regex keyword", method) - } - if regexVal.Kind() != KindBool { - return false, fmt.Errorf("string.%s regex keyword must be bool", method) - } - return regexVal.Bool(), nil -} - -func stringSub(text, pattern, replacement string, regex bool) (string, error) { - if !regex { - return strings.Replace(text, pattern, replacement, 1), nil - } - re, err := regexp.Compile(pattern) - if err != nil { - return "", err - } - loc := re.FindStringSubmatchIndex(text) - if loc == nil { - return text, nil - } - replaced := re.ExpandString(nil, replacement, text, loc) - return text[:loc[0]] + string(replaced) + text[loc[1]:], nil -} - -func stringGSub(text, pattern, replacement string, regex bool) (string, error) { - if !regex { - return strings.ReplaceAll(text, pattern, replacement), nil - } - re, err := regexp.Compile(pattern) - if err != nil { - return "", err - } - return re.ReplaceAllString(text, replacement), nil -} - -func stringBangResult(original, updated string) Value { - if updated == original { - return NewNil() - } - return NewString(updated) -} - -func stringSquish(text string) string { - return strings.Join(strings.Fields(text), " ") -} - -func stringTemplateOption(kwargs map[string]Value) (bool, error) { - if len(kwargs) == 0 { - return false, nil - } - value, ok := kwargs["strict"] - if !ok || len(kwargs) != 1 { - return false, fmt.Errorf("string.template supports only strict keyword") - } - if value.Kind() != KindBool { - return false, fmt.Errorf("string.template strict keyword must be bool") - } - return value.Bool(), nil -} - -func stringTemplateLookup(context Value, keyPath string) (Value, bool) { - current := context - for _, segment := range strings.Split(keyPath, ".") { - if segment == "" { - return NewNil(), false - } - if current.Kind() != KindHash && current.Kind() != KindObject { - return NewNil(), false - } - next, ok := current.Hash()[segment] - if !ok { - return NewNil(), false - } - current = next - } - return current, true -} - -func stringTemplateScalarValue(value Value, keyPath string) (string, error) { - switch value.Kind() { - case KindNil, KindBool, KindInt, KindFloat, KindString, KindSymbol, KindMoney, KindDuration, KindTime: - return value.String(), nil - default: - return "", fmt.Errorf("string.template placeholder %s value must be scalar", keyPath) - } -} - -func stringTemplate(text string, context Value, strict bool) (string, error) { - templateErr := error(nil) - rendered := stringTemplatePattern.ReplaceAllStringFunc(text, func(match string) string { - if templateErr != nil { - return match - } - submatch := stringTemplatePattern.FindStringSubmatch(match) - if len(submatch) != 2 { - return match - } - keyPath := submatch[1] - value, ok := stringTemplateLookup(context, keyPath) - if !ok { - if strict { - templateErr = fmt.Errorf("string.template missing placeholder %s", keyPath) - } - return match - } - segment, err := stringTemplateScalarValue(value, keyPath) - if err != nil { - templateErr = err - return match - } - return segment - }) - if templateErr != nil { - return "", templateErr - } - return rendered, nil -} - -func stringMember(str Value, property string) (Value, error) { - switch property { - case "size": - return NewAutoBuiltin("string.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.size does not take arguments") - } - return NewInt(int64(len([]rune(receiver.String())))), nil - }), nil - case "length": - return NewAutoBuiltin("string.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.length does not take arguments") - } - return NewInt(int64(len([]rune(receiver.String())))), nil - }), nil - case "bytesize": - return NewAutoBuiltin("string.bytesize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.bytesize does not take arguments") - } - return NewInt(int64(len(receiver.String()))), nil - }), nil - case "ord": - return NewAutoBuiltin("string.ord", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.ord does not take arguments") - } - runes := []rune(receiver.String()) - if len(runes) == 0 { - return NewNil(), fmt.Errorf("string.ord requires non-empty string") - } - return NewInt(int64(runes[0])), nil - }), nil - case "chr": - return NewAutoBuiltin("string.chr", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.chr does not take arguments") - } - runes := []rune(receiver.String()) - if len(runes) == 0 { - return NewNil(), nil - } - return NewString(string(runes[0])), nil - }), nil - case "empty?": - return NewAutoBuiltin("string.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.empty? does not take arguments") - } - return NewBool(len(receiver.String()) == 0), nil - }), nil - case "clear": - return NewAutoBuiltin("string.clear", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.clear does not take arguments") - } - return NewString(""), nil - }), nil - case "concat": - return NewAutoBuiltin("string.concat", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - var b strings.Builder - b.WriteString(receiver.String()) - for _, arg := range args { - if arg.Kind() != KindString { - return NewNil(), fmt.Errorf("string.concat expects string arguments") - } - b.WriteString(arg.String()) - } - return NewString(b.String()), nil - }), nil - case "replace": - return NewAutoBuiltin("string.replace", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.replace expects exactly one replacement") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.replace replacement must be string") - } - return NewString(args[0].String()), nil - }), nil - case "start_with?": - return NewAutoBuiltin("string.start_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.start_with? expects exactly one prefix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.start_with? prefix must be string") - } - return NewBool(strings.HasPrefix(receiver.String(), args[0].String())), nil - }), nil - case "end_with?": - return NewAutoBuiltin("string.end_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.end_with? expects exactly one suffix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.end_with? suffix must be string") - } - return NewBool(strings.HasSuffix(receiver.String(), args[0].String())), nil - }), nil - case "include?": - return NewAutoBuiltin("string.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.include? expects exactly one substring") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.include? substring must be string") - } - return NewBool(strings.Contains(receiver.String(), args[0].String())), nil - }), nil - case "match": - return NewAutoBuiltin("string.match", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("string.match does not take keyword arguments") - } - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.match expects exactly one pattern") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.match pattern must be string") - } - pattern := args[0].String() - re, err := regexp.Compile(pattern) - if err != nil { - return NewNil(), fmt.Errorf("string.match invalid regex: %v", err) - } - text := receiver.String() - indices := re.FindStringSubmatchIndex(text) - if indices == nil { - return NewNil(), nil - } - values := make([]Value, len(indices)/2) - for i := range values { - start := indices[i*2] - end := indices[i*2+1] - if start < 0 || end < 0 { - values[i] = NewNil() - continue - } - values[i] = NewString(text[start:end]) - } - return NewArray(values), nil - }), nil - case "scan": - return NewAutoBuiltin("string.scan", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(kwargs) > 0 { - return NewNil(), fmt.Errorf("string.scan does not take keyword arguments") - } - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.scan expects exactly one pattern") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.scan pattern must be string") - } - pattern := args[0].String() - re, err := regexp.Compile(pattern) - if err != nil { - return NewNil(), fmt.Errorf("string.scan invalid regex: %v", err) - } - matches := re.FindAllString(receiver.String(), -1) - values := make([]Value, len(matches)) - for i, m := range matches { - values[i] = NewString(m) - } - return NewArray(values), nil - }), nil - case "index": - return NewAutoBuiltin("string.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("string.index expects substring and optional offset") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.index substring must be string") - } - offset := 0 - if len(args) == 2 { - i, err := valueToInt(args[1]) - if err != nil || i < 0 { - return NewNil(), fmt.Errorf("string.index offset must be non-negative integer") - } - offset = i - } - index := stringRuneIndex(receiver.String(), args[0].String(), offset) - if index < 0 { - return NewNil(), nil - } - return NewInt(int64(index)), nil - }), nil - case "rindex": - return NewAutoBuiltin("string.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("string.rindex expects substring and optional offset") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.rindex substring must be string") - } - offset := len([]rune(receiver.String())) - if len(args) == 2 { - i, err := valueToInt(args[1]) - if err != nil || i < 0 { - return NewNil(), fmt.Errorf("string.rindex offset must be non-negative integer") - } - offset = i - } - index := stringRuneRIndex(receiver.String(), args[0].String(), offset) - if index < 0 { - return NewNil(), nil - } - return NewInt(int64(index)), nil - }), nil - case "slice": - return NewAutoBuiltin("string.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("string.slice expects index and optional length") - } - start, err := valueToInt(args[0]) - if err != nil { - return NewNil(), fmt.Errorf("string.slice index must be integer") - } - runes := []rune(receiver.String()) - if len(args) == 1 { - if start < 0 || start >= len(runes) { - return NewNil(), nil - } - return NewString(string(runes[start])), nil - } - length, err := valueToInt(args[1]) - if err != nil { - return NewNil(), fmt.Errorf("string.slice length must be integer") - } - substr, ok := stringRuneSlice(receiver.String(), start, length) - if !ok { - return NewNil(), nil - } - return NewString(substr), nil - }), nil - case "strip": - return NewAutoBuiltin("string.strip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.strip does not take arguments") - } - return NewString(strings.TrimSpace(receiver.String())), nil - }), nil - case "strip!": - return NewAutoBuiltin("string.strip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.strip! does not take arguments") - } - updated := strings.TrimSpace(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "squish": - return NewAutoBuiltin("string.squish", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.squish does not take arguments") - } - return NewString(stringSquish(receiver.String())), nil - }), nil - case "squish!": - return NewAutoBuiltin("string.squish!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.squish! does not take arguments") - } - updated := stringSquish(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "lstrip": - return NewAutoBuiltin("string.lstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.lstrip does not take arguments") - } - return NewString(strings.TrimLeftFunc(receiver.String(), unicode.IsSpace)), nil - }), nil - case "lstrip!": - return NewAutoBuiltin("string.lstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.lstrip! does not take arguments") - } - updated := strings.TrimLeftFunc(receiver.String(), unicode.IsSpace) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "rstrip": - return NewAutoBuiltin("string.rstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.rstrip does not take arguments") - } - return NewString(strings.TrimRightFunc(receiver.String(), unicode.IsSpace)), nil - }), nil - case "rstrip!": - return NewAutoBuiltin("string.rstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.rstrip! does not take arguments") - } - updated := strings.TrimRightFunc(receiver.String(), unicode.IsSpace) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "chomp": - return NewAutoBuiltin("string.chomp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("string.chomp accepts at most one separator") - } - text := receiver.String() - if len(args) == 0 { - return NewString(chompDefault(text)), nil - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.chomp separator must be string") - } - sep := args[0].String() - if sep == "" { - return NewString(strings.TrimRight(text, "\r\n")), nil - } - if strings.HasSuffix(text, sep) { - return NewString(text[:len(text)-len(sep)]), nil - } - return NewString(text), nil - }), nil - case "chomp!": - return NewAutoBuiltin("string.chomp!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("string.chomp! accepts at most one separator") - } - original := receiver.String() - if len(args) == 0 { - return stringBangResult(original, chompDefault(original)), nil - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.chomp! separator must be string") - } - sep := args[0].String() - if sep == "" { - return stringBangResult(original, strings.TrimRight(original, "\r\n")), nil - } - if strings.HasSuffix(original, sep) { - return stringBangResult(original, original[:len(original)-len(sep)]), nil - } - return NewNil(), nil - }), nil - case "delete_prefix": - return NewAutoBuiltin("string.delete_prefix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_prefix expects exactly one prefix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_prefix prefix must be string") - } - return NewString(strings.TrimPrefix(receiver.String(), args[0].String())), nil - }), nil - case "delete_prefix!": - return NewAutoBuiltin("string.delete_prefix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_prefix! expects exactly one prefix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_prefix! prefix must be string") - } - updated := strings.TrimPrefix(receiver.String(), args[0].String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "delete_suffix": - return NewAutoBuiltin("string.delete_suffix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_suffix expects exactly one suffix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_suffix suffix must be string") - } - return NewString(strings.TrimSuffix(receiver.String(), args[0].String())), nil - }), nil - case "delete_suffix!": - return NewAutoBuiltin("string.delete_suffix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.delete_suffix! expects exactly one suffix") - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.delete_suffix! suffix must be string") - } - updated := strings.TrimSuffix(receiver.String(), args[0].String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "upcase": - return NewAutoBuiltin("string.upcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.upcase does not take arguments") - } - return NewString(strings.ToUpper(receiver.String())), nil - }), nil - case "upcase!": - return NewAutoBuiltin("string.upcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.upcase! does not take arguments") - } - updated := strings.ToUpper(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "downcase": - return NewAutoBuiltin("string.downcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.downcase does not take arguments") - } - return NewString(strings.ToLower(receiver.String())), nil - }), nil - case "downcase!": - return NewAutoBuiltin("string.downcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.downcase! does not take arguments") - } - updated := strings.ToLower(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "capitalize": - return NewAutoBuiltin("string.capitalize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.capitalize does not take arguments") - } - return NewString(stringCapitalize(receiver.String())), nil - }), nil - case "capitalize!": - return NewAutoBuiltin("string.capitalize!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.capitalize! does not take arguments") - } - updated := stringCapitalize(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "swapcase": - return NewAutoBuiltin("string.swapcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.swapcase does not take arguments") - } - return NewString(stringSwapCase(receiver.String())), nil - }), nil - case "swapcase!": - return NewAutoBuiltin("string.swapcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.swapcase! does not take arguments") - } - updated := stringSwapCase(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "reverse": - return NewAutoBuiltin("string.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.reverse does not take arguments") - } - return NewString(stringReverse(receiver.String())), nil - }), nil - case "reverse!": - return NewAutoBuiltin("string.reverse!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("string.reverse! does not take arguments") - } - updated := stringReverse(receiver.String()) - return stringBangResult(receiver.String(), updated), nil - }), nil - case "sub": - return NewAutoBuiltin("string.sub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.sub expects pattern and replacement") - } - regex, err := stringRegexOption("sub", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub replacement must be string") - } - updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.sub invalid regex: %v", err) - } - return NewString(updated), nil - }), nil - case "sub!": - return NewAutoBuiltin("string.sub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.sub! expects pattern and replacement") - } - regex, err := stringRegexOption("sub!", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub! pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.sub! replacement must be string") - } - updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.sub! invalid regex: %v", err) - } - return stringBangResult(receiver.String(), updated), nil - }), nil - case "gsub": - return NewAutoBuiltin("string.gsub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.gsub expects pattern and replacement") - } - regex, err := stringRegexOption("gsub", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub replacement must be string") - } - updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.gsub invalid regex: %v", err) - } - return NewString(updated), nil - }), nil - case "gsub!": - return NewAutoBuiltin("string.gsub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("string.gsub! expects pattern and replacement") - } - regex, err := stringRegexOption("gsub!", kwargs) - if err != nil { - return NewNil(), err - } - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub! pattern must be string") - } - if args[1].Kind() != KindString { - return NewNil(), fmt.Errorf("string.gsub! replacement must be string") - } - updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) - if err != nil { - return NewNil(), fmt.Errorf("string.gsub! invalid regex: %v", err) - } - return stringBangResult(receiver.String(), updated), nil - }), nil - case "split": - return NewAutoBuiltin("string.split", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("string.split accepts at most one separator") - } - text := receiver.String() - var parts []string - if len(args) == 0 { - parts = strings.Fields(text) - } else { - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("string.split separator must be string") - } - parts = strings.Split(text, args[0].String()) - } - values := make([]Value, len(parts)) - for i, part := range parts { - values[i] = NewString(part) - } - return NewArray(values), nil - }), nil - case "template": - return NewAutoBuiltin("string.template", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("string.template expects exactly one context hash") - } - if args[0].Kind() != KindHash && args[0].Kind() != KindObject { - return NewNil(), fmt.Errorf("string.template context must be hash") - } - strict, err := stringTemplateOption(kwargs) - if err != nil { - return NewNil(), err - } - rendered, err := stringTemplate(receiver.String(), args[0], strict) - if err != nil { - return NewNil(), err - } - return NewString(rendered), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown string method %s", property) - } -} - -func durationMember(d Duration, property string, pos Position) (Value, error) { - switch property { - case "seconds", "second": - return NewInt(d.Seconds()), nil - case "minutes", "minute": - return NewInt(d.Seconds() / 60), nil - case "hours", "hour": - return NewInt(d.Seconds() / 3600), nil - case "days", "day": - return NewInt(d.Seconds() / 86400), nil - case "weeks", "week": - return NewInt(d.Seconds() / 604800), nil - case "in_seconds": - return NewFloat(float64(d.Seconds())), nil - case "in_minutes": - return NewFloat(float64(d.Seconds()) / 60), nil - case "in_hours": - return NewFloat(float64(d.Seconds()) / 3600), nil - case "in_days": - return NewFloat(float64(d.Seconds()) / 86400), nil - case "in_weeks": - return NewFloat(float64(d.Seconds()) / 604800), nil - case "in_months": - return NewFloat(float64(d.Seconds()) / (30 * 86400)), nil - case "in_years": - return NewFloat(float64(d.Seconds()) / (365 * 86400)), nil - case "iso8601": - return NewString(d.iso8601()), nil - case "parts": - p := d.parts() - return NewHash(map[string]Value{ - "days": NewInt(p["days"]), - "hours": NewInt(p["hours"]), - "minutes": NewInt(p["minutes"]), - "seconds": NewInt(p["seconds"]), - }), nil - case "to_i": - return NewInt(d.Seconds()), nil - case "to_s": - return NewString(d.String()), nil - case "format": - return NewString(d.String()), nil - case "eql?": - return NewBuiltin("duration.eql?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 || args[0].Kind() != KindDuration { - return NewNil(), fmt.Errorf("duration.eql? expects a duration") - } - return NewBool(d.Seconds() == args[0].Duration().Seconds()), nil - }), nil - case "after", "since", "from_now": - return NewBuiltin("duration.after", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - start, err := durationTimeArg(args, true, "after") - if err != nil { - return NewNil(), err - } - result := start.Add(time.Duration(d.Seconds()) * time.Second).UTC() - return NewTime(result), nil - }), nil - case "ago", "before", "until": - return NewBuiltin("duration.before", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - start, err := durationTimeArg(args, true, "before") - if err != nil { - return NewNil(), err - } - result := start.Add(-time.Duration(d.Seconds()) * time.Second).UTC() - return NewTime(result), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown duration method %s", property) - } -} - -func durationTimeArg(args []Value, allowEmpty bool, name string) (time.Time, error) { - if len(args) == 0 { - if allowEmpty { - return time.Now().UTC(), nil - } - return time.Time{}, fmt.Errorf("%s expects a time argument", name) - } - if len(args) != 1 { - return time.Time{}, fmt.Errorf("%s expects at most one time argument", name) - } - val := args[0] - switch val.Kind() { - case KindString: - t, err := time.Parse(time.RFC3339, val.String()) - if err != nil { - return time.Time{}, fmt.Errorf("invalid time: %v", err) - } - return t.UTC(), nil - case KindTime: - return val.Time(), nil - default: - return time.Time{}, fmt.Errorf("%s expects a Time or RFC3339 string", name) - } -} - -func arrayMember(array Value, property string) (Value, error) { - switch property { - case "size": - return NewAutoBuiltin("array.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.size does not take arguments") - } - return NewInt(int64(len(receiver.Array()))), nil - }), nil - case "each": - return NewAutoBuiltin("array.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.each"); err != nil { - return NewNil(), err - } - for _, item := range receiver.Array() { - if _, err := exec.CallBlock(block, []Value{item}); err != nil { - return NewNil(), err - } - } - return receiver, nil - }), nil - case "map": - return NewAutoBuiltin("array.map", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.map"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - result := make([]Value, len(arr)) - for i, item := range arr { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - result[i] = val - } - return NewArray(result), nil - }), nil - case "select": - return NewAutoBuiltin("array.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.select"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - out := make([]Value, 0, len(arr)) - for _, item := range arr { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if val.Truthy() { - out = append(out, item) - } - } - return NewArray(out), nil - }), nil - case "find": - return NewAutoBuiltin("array.find", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.find does not take arguments") - } - if err := ensureBlock(block, "array.find"); err != nil { - return NewNil(), err - } - for _, item := range receiver.Array() { - match, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if match.Truthy() { - return item, nil - } - } - return NewNil(), nil - }), nil - case "find_index": - return NewAutoBuiltin("array.find_index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.find_index does not take arguments") - } - if err := ensureBlock(block, "array.find_index"); err != nil { - return NewNil(), err - } - for idx, item := range receiver.Array() { - match, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if match.Truthy() { - return NewInt(int64(idx)), nil - } - } - return NewNil(), nil - }), nil - case "reduce": - return NewAutoBuiltin("array.reduce", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := ensureBlock(block, "array.reduce"); err != nil { - return NewNil(), err - } - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.reduce accepts at most one initial value") - } - arr := receiver.Array() - if len(arr) == 0 && len(args) == 0 { - return NewNil(), fmt.Errorf("array.reduce on empty array requires an initial value") - } - var acc Value - start := 0 - if len(args) == 1 { - acc = args[0] - } else { - acc = arr[0] - start = 1 - } - for i := start; i < len(arr); i++ { - next, err := exec.CallBlock(block, []Value{acc, arr[i]}) - if err != nil { - return NewNil(), err - } - acc = next - } - return acc, nil - }), nil - case "include?": - return NewAutoBuiltin("array.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("array.include? expects exactly one value") - } - for _, item := range receiver.Array() { - if item.Equal(args[0]) { - return NewBool(true), nil - } - } - return NewBool(false), nil - }), nil - case "index": - return NewAutoBuiltin("array.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("array.index expects value and optional offset") - } - offset := 0 - if len(args) == 2 { - n, err := valueToInt(args[1]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.index offset must be non-negative integer") - } - offset = n - } - arr := receiver.Array() - if offset >= len(arr) { - return NewNil(), nil - } - for idx := offset; idx < len(arr); idx++ { - if arr[idx].Equal(args[0]) { - return NewInt(int64(idx)), nil - } - } - return NewNil(), nil - }), nil - case "rindex": - return NewAutoBuiltin("array.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("array.rindex expects value and optional offset") - } - offset := -1 - if len(args) == 2 { - n, err := valueToInt(args[1]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.rindex offset must be non-negative integer") - } - offset = n - } - arr := receiver.Array() - if len(arr) == 0 { - return NewNil(), nil - } - if offset < 0 { - offset = len(arr) - 1 - } - if offset >= len(arr) { - offset = len(arr) - 1 - } - for idx := offset; idx >= 0; idx-- { - if arr[idx].Equal(args[0]) { - return NewInt(int64(idx)), nil - } - } - return NewNil(), nil - }), nil - case "count": - return NewAutoBuiltin("array.count", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.count accepts at most one value argument") - } - arr := receiver.Array() - if len(args) == 1 { - if block.Block() != nil { - return NewNil(), fmt.Errorf("array.count does not accept both argument and block") - } - total := int64(0) - for _, item := range arr { - if item.Equal(args[0]) { - total++ - } - } - return NewInt(total), nil - } - if block.Block() == nil { - return NewInt(int64(len(arr))), nil - } - total := int64(0) - for _, item := range arr { - include, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if include.Truthy() { - total++ - } - } - return NewInt(total), nil - }), nil - case "any?": - return NewAutoBuiltin("array.any?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.any? does not take arguments") - } - for _, item := range receiver.Array() { - if block.Block() != nil { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if val.Truthy() { - return NewBool(true), nil - } - continue - } - if item.Truthy() { - return NewBool(true), nil - } - } - return NewBool(false), nil - }), nil - case "all?": - return NewAutoBuiltin("array.all?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.all? does not take arguments") - } - for _, item := range receiver.Array() { - if block.Block() != nil { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if !val.Truthy() { - return NewBool(false), nil - } - continue - } - if !item.Truthy() { - return NewBool(false), nil - } - } - return NewBool(true), nil - }), nil - case "none?": - return NewAutoBuiltin("array.none?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.none? does not take arguments") - } - for _, item := range receiver.Array() { - if block.Block() != nil { - val, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if val.Truthy() { - return NewBool(false), nil - } - continue - } - if item.Truthy() { - return NewBool(false), nil - } - } - return NewBool(true), nil - }), nil - case "push": - return NewBuiltin("array.push", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) == 0 { - return NewNil(), fmt.Errorf("array.push expects at least one argument") - } - base := receiver.Array() - out := make([]Value, len(base)+len(args)) - copy(out, base) - copy(out[len(base):], args) - return NewArray(out), nil - }), nil - case "pop": - return NewAutoBuiltin("array.pop", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.pop accepts at most one argument") - } - count := 1 - if len(args) == 1 { - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.pop expects non-negative integer") - } - count = n - } - arr := receiver.Array() - if count == 0 { - return NewHash(map[string]Value{ - "array": NewArray(arr), - "popped": NewNil(), - }), nil - } - if len(arr) == 0 { - popped := NewNil() - if len(args) == 1 { - popped = NewArray([]Value{}) - } - return NewHash(map[string]Value{ - "array": NewArray([]Value{}), - "popped": popped, - }), nil - } - if count > len(arr) { - count = len(arr) - } - remaining := make([]Value, len(arr)-count) - copy(remaining, arr[:len(arr)-count]) - removed := make([]Value, count) - copy(removed, arr[len(arr)-count:]) - result := map[string]Value{ - "array": NewArray(remaining), - } - if count == 1 && len(args) == 0 { - result["popped"] = removed[0] - } else { - result["popped"] = NewArray(removed) - } - return NewHash(result), nil - }), nil - case "uniq": - return NewAutoBuiltin("array.uniq", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.uniq does not take arguments") - } - arr := receiver.Array() - unique := make([]Value, 0, len(arr)) - for _, item := range arr { - found := slices.ContainsFunc(unique, item.Equal) - if !found { - unique = append(unique, item) - } - } - return NewArray(unique), nil - }), nil - case "first": - return NewAutoBuiltin("array.first", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - arr := receiver.Array() - if len(args) == 0 { - if len(arr) == 0 { - return NewNil(), nil - } - return arr[0], nil - } - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.first expects non-negative integer") - } - if n > len(arr) { - n = len(arr) - } - out := make([]Value, n) - copy(out, arr[:n]) - return NewArray(out), nil - }), nil - case "last": - return NewAutoBuiltin("array.last", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - arr := receiver.Array() - if len(args) == 0 { - if len(arr) == 0 { - return NewNil(), nil - } - return arr[len(arr)-1], nil - } - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.last expects non-negative integer") - } - if n > len(arr) { - n = len(arr) - } - out := make([]Value, n) - copy(out, arr[len(arr)-n:]) - return NewArray(out), nil - }), nil - case "sum": - return NewAutoBuiltin("array.sum", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - arr := receiver.Array() - total := NewInt(0) - for _, item := range arr { - switch item.Kind() { - case KindInt, KindFloat: - default: - return NewNil(), fmt.Errorf("array.sum supports numeric values") - } - sum, err := addValues(total, item) - if err != nil { - return NewNil(), fmt.Errorf("array.sum supports numeric values") - } - total = sum - } - return total, nil - }), nil - case "compact": - return NewAutoBuiltin("array.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.compact does not take arguments") - } - arr := receiver.Array() - out := make([]Value, 0, len(arr)) - for _, item := range arr { - if item.Kind() != KindNil { - out = append(out, item) - } - } - return NewArray(out), nil - }), nil - case "flatten": - return NewAutoBuiltin("array.flatten", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - // depth=-1 is a sentinel value meaning "flatten fully" (no depth limit) - depth := -1 - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.flatten accepts at most one depth argument") - } - if len(args) == 1 { - n, err := valueToInt(args[0]) - if err != nil || n < 0 { - return NewNil(), fmt.Errorf("array.flatten depth must be non-negative integer") - } - depth = n - } - arr := receiver.Array() - out := flattenValues(arr, depth) - return NewArray(out), nil - }), nil - case "chunk": - return NewAutoBuiltin("array.chunk", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("array.chunk expects a chunk size") - } - sizeValue := args[0] - maxNativeInt := int64(^uint(0) >> 1) - if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { - return NewNil(), fmt.Errorf("array.chunk size must be a positive integer") - } - size := int(sizeValue.Int()) - arr := receiver.Array() - if len(arr) == 0 { - return NewArray([]Value{}), nil - } - chunkCapacity := len(arr) / size - if len(arr)%size != 0 { - chunkCapacity++ - } - chunks := make([]Value, 0, chunkCapacity) - for i := 0; i < len(arr); i += size { - end := i + size - if end > len(arr) { - end = len(arr) - } - part := make([]Value, end-i) - copy(part, arr[i:end]) - chunks = append(chunks, NewArray(part)) - } - return NewArray(chunks), nil - }), nil - case "window": - return NewAutoBuiltin("array.window", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 1 { - return NewNil(), fmt.Errorf("array.window expects a window size") - } - sizeValue := args[0] - maxNativeInt := int64(^uint(0) >> 1) - if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { - return NewNil(), fmt.Errorf("array.window size must be a positive integer") - } - size := int(sizeValue.Int()) - arr := receiver.Array() - if size > len(arr) { - return NewArray([]Value{}), nil - } - windows := make([]Value, 0, len(arr)-size+1) - for i := 0; i+size <= len(arr); i++ { - part := make([]Value, size) - copy(part, arr[i:i+size]) - windows = append(windows, NewArray(part)) - } - return NewArray(windows), nil - }), nil - case "join": - return NewAutoBuiltin("array.join", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 1 { - return NewNil(), fmt.Errorf("array.join accepts at most one separator") - } - sep := "" - if len(args) == 1 { - if args[0].Kind() != KindString { - return NewNil(), fmt.Errorf("array.join separator must be string") - } - sep = args[0].String() - } - arr := receiver.Array() - if len(arr) == 0 { - return NewString(""), nil - } - // Use strings.Builder for efficient concatenation - var b strings.Builder - for i, item := range arr { - if i > 0 { - b.WriteString(sep) - } - b.WriteString(item.String()) - } - return NewString(b.String()), nil - }), nil - case "reverse": - return NewAutoBuiltin("array.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.reverse does not take arguments") - } - arr := receiver.Array() - out := make([]Value, len(arr)) - for i, item := range arr { - out[len(arr)-1-i] = item - } - return NewArray(out), nil - }), nil - case "sort": - return NewAutoBuiltin("array.sort", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.sort does not take arguments") - } - arr := receiver.Array() - out := make([]Value, len(arr)) - copy(out, arr) - var sortErr error - sort.SliceStable(out, func(i, j int) bool { - if sortErr != nil { - return false - } - if block.Block() != nil { - cmpValue, err := exec.CallBlock(block, []Value{out[i], out[j]}) - if err != nil { - sortErr = err - return false - } - cmp, err := sortComparisonResult(cmpValue) - if err != nil { - sortErr = fmt.Errorf("array.sort block must return numeric comparator") - return false - } - return cmp < 0 - } - cmp, err := arraySortCompareValues(out[i], out[j]) - if err != nil { - sortErr = fmt.Errorf("array.sort values are not comparable") - return false - } - return cmp < 0 - }) - if sortErr != nil { - return NewNil(), sortErr - } - return NewArray(out), nil - }), nil - case "sort_by": - return NewAutoBuiltin("array.sort_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.sort_by does not take arguments") - } - if err := ensureBlock(block, "array.sort_by"); err != nil { - return NewNil(), err - } - type itemWithSortKey struct { - item Value - key Value - index int - } - arr := receiver.Array() - withKeys := make([]itemWithSortKey, len(arr)) - for i, item := range arr { - sortKey, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - withKeys[i] = itemWithSortKey{item: item, key: sortKey, index: i} - } - var sortErr error - sort.SliceStable(withKeys, func(i, j int) bool { - if sortErr != nil { - return false - } - cmp, err := arraySortCompareValues(withKeys[i].key, withKeys[j].key) - if err != nil { - sortErr = fmt.Errorf("array.sort_by block values are not comparable") - return false - } - if cmp == 0 { - return withKeys[i].index < withKeys[j].index - } - return cmp < 0 - }) - if sortErr != nil { - return NewNil(), sortErr - } - out := make([]Value, len(withKeys)) - for i, item := range withKeys { - out[i] = item.item - } - return NewArray(out), nil - }), nil - case "partition": - return NewAutoBuiltin("array.partition", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.partition does not take arguments") - } - if err := ensureBlock(block, "array.partition"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - left := make([]Value, 0, len(arr)) - right := make([]Value, 0, len(arr)) - for _, item := range arr { - match, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - if match.Truthy() { - left = append(left, item) - } else { - right = append(right, item) - } - } - return NewArray([]Value{NewArray(left), NewArray(right)}), nil - }), nil - case "group_by": - return NewAutoBuiltin("array.group_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.group_by does not take arguments") - } - if err := ensureBlock(block, "array.group_by"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - groups := make(map[string][]Value, len(arr)) - for _, item := range arr { - groupValue, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - key, err := valueToHashKey(groupValue) - if err != nil { - return NewNil(), fmt.Errorf("array.group_by block must return symbol or string") - } - groups[key] = append(groups[key], item) - } - result := make(map[string]Value, len(groups)) - for key, items := range groups { - result[key] = NewArray(items) - } - return NewHash(result), nil - }), nil - case "group_by_stable": - return NewAutoBuiltin("array.group_by_stable", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.group_by_stable does not take arguments") - } - if err := ensureBlock(block, "array.group_by_stable"); err != nil { - return NewNil(), err - } - arr := receiver.Array() - order := make([]string, 0, len(arr)) - keyValues := make(map[string]Value, len(arr)) - groups := make(map[string][]Value, len(arr)) - for _, item := range arr { - groupValue, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - key, err := valueToHashKey(groupValue) - if err != nil { - return NewNil(), fmt.Errorf("array.group_by_stable block must return symbol or string") - } - if _, exists := groups[key]; !exists { - order = append(order, key) - keyValues[key] = groupValue - groups[key] = []Value{} - } - groups[key] = append(groups[key], item) - } - result := make([]Value, 0, len(order)) - for _, key := range order { - result = append(result, NewArray([]Value{ - keyValues[key], - NewArray(groups[key]), - })) - } - return NewArray(result), nil - }), nil - case "tally": - return NewAutoBuiltin("array.tally", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) > 0 { - return NewNil(), fmt.Errorf("array.tally does not take arguments") - } - arr := receiver.Array() - counts := make(map[string]int64, len(arr)) - for _, item := range arr { - keyValue := item - if block.Block() != nil { - mapped, err := exec.CallBlock(block, []Value{item}) - if err != nil { - return NewNil(), err - } - keyValue = mapped - } - key, err := valueToHashKey(keyValue) - if err != nil { - return NewNil(), fmt.Errorf("array.tally values must be symbol or string") - } - counts[key]++ - } - result := make(map[string]Value, len(counts)) - for key, count := range counts { - result[key] = NewInt(count) - } - return NewHash(result), nil - }), nil - default: - return NewNil(), fmt.Errorf("unknown array method %s", property) - } -} diff --git a/vibes/execution_members_array.go b/vibes/execution_members_array.go new file mode 100644 index 0000000..edb32d1 --- /dev/null +++ b/vibes/execution_members_array.go @@ -0,0 +1,755 @@ +package vibes + +import ( + "fmt" + "slices" + "sort" + "strings" +) + +func arrayMember(array Value, property string) (Value, error) { + switch property { + case "size": + return NewAutoBuiltin("array.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.size does not take arguments") + } + return NewInt(int64(len(receiver.Array()))), nil + }), nil + case "each": + return NewAutoBuiltin("array.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.each"); err != nil { + return NewNil(), err + } + for _, item := range receiver.Array() { + if _, err := exec.CallBlock(block, []Value{item}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "map": + return NewAutoBuiltin("array.map", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.map"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + result := make([]Value, len(arr)) + for i, item := range arr { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + result[i] = val + } + return NewArray(result), nil + }), nil + case "select": + return NewAutoBuiltin("array.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.select"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + out := make([]Value, 0, len(arr)) + for _, item := range arr { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if val.Truthy() { + out = append(out, item) + } + } + return NewArray(out), nil + }), nil + case "find": + return NewAutoBuiltin("array.find", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.find does not take arguments") + } + if err := ensureBlock(block, "array.find"); err != nil { + return NewNil(), err + } + for _, item := range receiver.Array() { + match, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if match.Truthy() { + return item, nil + } + } + return NewNil(), nil + }), nil + case "find_index": + return NewAutoBuiltin("array.find_index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.find_index does not take arguments") + } + if err := ensureBlock(block, "array.find_index"); err != nil { + return NewNil(), err + } + for idx, item := range receiver.Array() { + match, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if match.Truthy() { + return NewInt(int64(idx)), nil + } + } + return NewNil(), nil + }), nil + case "reduce": + return NewAutoBuiltin("array.reduce", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := ensureBlock(block, "array.reduce"); err != nil { + return NewNil(), err + } + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.reduce accepts at most one initial value") + } + arr := receiver.Array() + if len(arr) == 0 && len(args) == 0 { + return NewNil(), fmt.Errorf("array.reduce on empty array requires an initial value") + } + var acc Value + start := 0 + if len(args) == 1 { + acc = args[0] + } else { + acc = arr[0] + start = 1 + } + for i := start; i < len(arr); i++ { + next, err := exec.CallBlock(block, []Value{acc, arr[i]}) + if err != nil { + return NewNil(), err + } + acc = next + } + return acc, nil + }), nil + case "include?": + return NewAutoBuiltin("array.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("array.include? expects exactly one value") + } + for _, item := range receiver.Array() { + if item.Equal(args[0]) { + return NewBool(true), nil + } + } + return NewBool(false), nil + }), nil + case "index": + return NewAutoBuiltin("array.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("array.index expects value and optional offset") + } + offset := 0 + if len(args) == 2 { + n, err := valueToInt(args[1]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.index offset must be non-negative integer") + } + offset = n + } + arr := receiver.Array() + if offset >= len(arr) { + return NewNil(), nil + } + for idx := offset; idx < len(arr); idx++ { + if arr[idx].Equal(args[0]) { + return NewInt(int64(idx)), nil + } + } + return NewNil(), nil + }), nil + case "rindex": + return NewAutoBuiltin("array.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("array.rindex expects value and optional offset") + } + offset := -1 + if len(args) == 2 { + n, err := valueToInt(args[1]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.rindex offset must be non-negative integer") + } + offset = n + } + arr := receiver.Array() + if len(arr) == 0 { + return NewNil(), nil + } + if offset < 0 { + offset = len(arr) - 1 + } + if offset >= len(arr) { + offset = len(arr) - 1 + } + for idx := offset; idx >= 0; idx-- { + if arr[idx].Equal(args[0]) { + return NewInt(int64(idx)), nil + } + } + return NewNil(), nil + }), nil + case "count": + return NewAutoBuiltin("array.count", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.count accepts at most one value argument") + } + arr := receiver.Array() + if len(args) == 1 { + if block.Block() != nil { + return NewNil(), fmt.Errorf("array.count does not accept both argument and block") + } + total := int64(0) + for _, item := range arr { + if item.Equal(args[0]) { + total++ + } + } + return NewInt(total), nil + } + if block.Block() == nil { + return NewInt(int64(len(arr))), nil + } + total := int64(0) + for _, item := range arr { + include, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if include.Truthy() { + total++ + } + } + return NewInt(total), nil + }), nil + case "any?": + return NewAutoBuiltin("array.any?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.any? does not take arguments") + } + for _, item := range receiver.Array() { + if block.Block() != nil { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if val.Truthy() { + return NewBool(true), nil + } + continue + } + if item.Truthy() { + return NewBool(true), nil + } + } + return NewBool(false), nil + }), nil + case "all?": + return NewAutoBuiltin("array.all?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.all? does not take arguments") + } + for _, item := range receiver.Array() { + if block.Block() != nil { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if !val.Truthy() { + return NewBool(false), nil + } + continue + } + if !item.Truthy() { + return NewBool(false), nil + } + } + return NewBool(true), nil + }), nil + case "none?": + return NewAutoBuiltin("array.none?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.none? does not take arguments") + } + for _, item := range receiver.Array() { + if block.Block() != nil { + val, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if val.Truthy() { + return NewBool(false), nil + } + continue + } + if item.Truthy() { + return NewBool(false), nil + } + } + return NewBool(true), nil + }), nil + case "push": + return NewBuiltin("array.push", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) == 0 { + return NewNil(), fmt.Errorf("array.push expects at least one argument") + } + base := receiver.Array() + out := make([]Value, len(base)+len(args)) + copy(out, base) + copy(out[len(base):], args) + return NewArray(out), nil + }), nil + case "pop": + return NewAutoBuiltin("array.pop", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.pop accepts at most one argument") + } + count := 1 + if len(args) == 1 { + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.pop expects non-negative integer") + } + count = n + } + arr := receiver.Array() + if count == 0 { + return NewHash(map[string]Value{ + "array": NewArray(arr), + "popped": NewNil(), + }), nil + } + if len(arr) == 0 { + popped := NewNil() + if len(args) == 1 { + popped = NewArray([]Value{}) + } + return NewHash(map[string]Value{ + "array": NewArray([]Value{}), + "popped": popped, + }), nil + } + if count > len(arr) { + count = len(arr) + } + remaining := make([]Value, len(arr)-count) + copy(remaining, arr[:len(arr)-count]) + removed := make([]Value, count) + copy(removed, arr[len(arr)-count:]) + result := map[string]Value{ + "array": NewArray(remaining), + } + if count == 1 && len(args) == 0 { + result["popped"] = removed[0] + } else { + result["popped"] = NewArray(removed) + } + return NewHash(result), nil + }), nil + case "uniq": + return NewAutoBuiltin("array.uniq", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.uniq does not take arguments") + } + arr := receiver.Array() + unique := make([]Value, 0, len(arr)) + for _, item := range arr { + found := slices.ContainsFunc(unique, item.Equal) + if !found { + unique = append(unique, item) + } + } + return NewArray(unique), nil + }), nil + case "first": + return NewAutoBuiltin("array.first", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + arr := receiver.Array() + if len(args) == 0 { + if len(arr) == 0 { + return NewNil(), nil + } + return arr[0], nil + } + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.first expects non-negative integer") + } + if n > len(arr) { + n = len(arr) + } + out := make([]Value, n) + copy(out, arr[:n]) + return NewArray(out), nil + }), nil + case "last": + return NewAutoBuiltin("array.last", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + arr := receiver.Array() + if len(args) == 0 { + if len(arr) == 0 { + return NewNil(), nil + } + return arr[len(arr)-1], nil + } + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.last expects non-negative integer") + } + if n > len(arr) { + n = len(arr) + } + out := make([]Value, n) + copy(out, arr[len(arr)-n:]) + return NewArray(out), nil + }), nil + case "sum": + return NewAutoBuiltin("array.sum", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + arr := receiver.Array() + total := NewInt(0) + for _, item := range arr { + switch item.Kind() { + case KindInt, KindFloat: + default: + return NewNil(), fmt.Errorf("array.sum supports numeric values") + } + sum, err := addValues(total, item) + if err != nil { + return NewNil(), fmt.Errorf("array.sum supports numeric values") + } + total = sum + } + return total, nil + }), nil + case "compact": + return NewAutoBuiltin("array.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.compact does not take arguments") + } + arr := receiver.Array() + out := make([]Value, 0, len(arr)) + for _, item := range arr { + if item.Kind() != KindNil { + out = append(out, item) + } + } + return NewArray(out), nil + }), nil + case "flatten": + return NewAutoBuiltin("array.flatten", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + // depth=-1 is a sentinel value meaning "flatten fully" (no depth limit) + depth := -1 + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.flatten accepts at most one depth argument") + } + if len(args) == 1 { + n, err := valueToInt(args[0]) + if err != nil || n < 0 { + return NewNil(), fmt.Errorf("array.flatten depth must be non-negative integer") + } + depth = n + } + arr := receiver.Array() + out := flattenValues(arr, depth) + return NewArray(out), nil + }), nil + case "chunk": + return NewAutoBuiltin("array.chunk", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("array.chunk expects a chunk size") + } + sizeValue := args[0] + maxNativeInt := int64(^uint(0) >> 1) + if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { + return NewNil(), fmt.Errorf("array.chunk size must be a positive integer") + } + size := int(sizeValue.Int()) + arr := receiver.Array() + if len(arr) == 0 { + return NewArray([]Value{}), nil + } + chunkCapacity := len(arr) / size + if len(arr)%size != 0 { + chunkCapacity++ + } + chunks := make([]Value, 0, chunkCapacity) + for i := 0; i < len(arr); i += size { + end := i + size + if end > len(arr) { + end = len(arr) + } + part := make([]Value, end-i) + copy(part, arr[i:end]) + chunks = append(chunks, NewArray(part)) + } + return NewArray(chunks), nil + }), nil + case "window": + return NewAutoBuiltin("array.window", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("array.window expects a window size") + } + sizeValue := args[0] + maxNativeInt := int64(^uint(0) >> 1) + if sizeValue.Kind() != KindInt || sizeValue.Int() <= 0 || sizeValue.Int() > maxNativeInt { + return NewNil(), fmt.Errorf("array.window size must be a positive integer") + } + size := int(sizeValue.Int()) + arr := receiver.Array() + if size > len(arr) { + return NewArray([]Value{}), nil + } + windows := make([]Value, 0, len(arr)-size+1) + for i := 0; i+size <= len(arr); i++ { + part := make([]Value, size) + copy(part, arr[i:i+size]) + windows = append(windows, NewArray(part)) + } + return NewArray(windows), nil + }), nil + case "join": + return NewAutoBuiltin("array.join", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("array.join accepts at most one separator") + } + sep := "" + if len(args) == 1 { + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("array.join separator must be string") + } + sep = args[0].String() + } + arr := receiver.Array() + if len(arr) == 0 { + return NewString(""), nil + } + // Use strings.Builder for efficient concatenation + var b strings.Builder + for i, item := range arr { + if i > 0 { + b.WriteString(sep) + } + b.WriteString(item.String()) + } + return NewString(b.String()), nil + }), nil + case "reverse": + return NewAutoBuiltin("array.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.reverse does not take arguments") + } + arr := receiver.Array() + out := make([]Value, len(arr)) + for i, item := range arr { + out[len(arr)-1-i] = item + } + return NewArray(out), nil + }), nil + case "sort": + return NewAutoBuiltin("array.sort", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.sort does not take arguments") + } + arr := receiver.Array() + out := make([]Value, len(arr)) + copy(out, arr) + var sortErr error + sort.SliceStable(out, func(i, j int) bool { + if sortErr != nil { + return false + } + if block.Block() != nil { + cmpValue, err := exec.CallBlock(block, []Value{out[i], out[j]}) + if err != nil { + sortErr = err + return false + } + cmp, err := sortComparisonResult(cmpValue) + if err != nil { + sortErr = fmt.Errorf("array.sort block must return numeric comparator") + return false + } + return cmp < 0 + } + cmp, err := arraySortCompareValues(out[i], out[j]) + if err != nil { + sortErr = fmt.Errorf("array.sort values are not comparable") + return false + } + return cmp < 0 + }) + if sortErr != nil { + return NewNil(), sortErr + } + return NewArray(out), nil + }), nil + case "sort_by": + return NewAutoBuiltin("array.sort_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.sort_by does not take arguments") + } + if err := ensureBlock(block, "array.sort_by"); err != nil { + return NewNil(), err + } + type itemWithSortKey struct { + item Value + key Value + index int + } + arr := receiver.Array() + withKeys := make([]itemWithSortKey, len(arr)) + for i, item := range arr { + sortKey, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + withKeys[i] = itemWithSortKey{item: item, key: sortKey, index: i} + } + var sortErr error + sort.SliceStable(withKeys, func(i, j int) bool { + if sortErr != nil { + return false + } + cmp, err := arraySortCompareValues(withKeys[i].key, withKeys[j].key) + if err != nil { + sortErr = fmt.Errorf("array.sort_by block values are not comparable") + return false + } + if cmp == 0 { + return withKeys[i].index < withKeys[j].index + } + return cmp < 0 + }) + if sortErr != nil { + return NewNil(), sortErr + } + out := make([]Value, len(withKeys)) + for i, item := range withKeys { + out[i] = item.item + } + return NewArray(out), nil + }), nil + case "partition": + return NewAutoBuiltin("array.partition", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.partition does not take arguments") + } + if err := ensureBlock(block, "array.partition"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + left := make([]Value, 0, len(arr)) + right := make([]Value, 0, len(arr)) + for _, item := range arr { + match, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + if match.Truthy() { + left = append(left, item) + } else { + right = append(right, item) + } + } + return NewArray([]Value{NewArray(left), NewArray(right)}), nil + }), nil + case "group_by": + return NewAutoBuiltin("array.group_by", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.group_by does not take arguments") + } + if err := ensureBlock(block, "array.group_by"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + groups := make(map[string][]Value, len(arr)) + for _, item := range arr { + groupValue, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + key, err := valueToHashKey(groupValue) + if err != nil { + return NewNil(), fmt.Errorf("array.group_by block must return symbol or string") + } + groups[key] = append(groups[key], item) + } + result := make(map[string]Value, len(groups)) + for key, items := range groups { + result[key] = NewArray(items) + } + return NewHash(result), nil + }), nil + case "group_by_stable": + return NewAutoBuiltin("array.group_by_stable", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.group_by_stable does not take arguments") + } + if err := ensureBlock(block, "array.group_by_stable"); err != nil { + return NewNil(), err + } + arr := receiver.Array() + order := make([]string, 0, len(arr)) + keyValues := make(map[string]Value, len(arr)) + groups := make(map[string][]Value, len(arr)) + for _, item := range arr { + groupValue, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + key, err := valueToHashKey(groupValue) + if err != nil { + return NewNil(), fmt.Errorf("array.group_by_stable block must return symbol or string") + } + if _, exists := groups[key]; !exists { + order = append(order, key) + keyValues[key] = groupValue + groups[key] = []Value{} + } + groups[key] = append(groups[key], item) + } + result := make([]Value, 0, len(order)) + for _, key := range order { + result = append(result, NewArray([]Value{ + keyValues[key], + NewArray(groups[key]), + })) + } + return NewArray(result), nil + }), nil + case "tally": + return NewAutoBuiltin("array.tally", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("array.tally does not take arguments") + } + arr := receiver.Array() + counts := make(map[string]int64, len(arr)) + for _, item := range arr { + keyValue := item + if block.Block() != nil { + mapped, err := exec.CallBlock(block, []Value{item}) + if err != nil { + return NewNil(), err + } + keyValue = mapped + } + key, err := valueToHashKey(keyValue) + if err != nil { + return NewNil(), fmt.Errorf("array.tally values must be symbol or string") + } + counts[key]++ + } + result := make(map[string]Value, len(counts)) + for key, count := range counts { + result[key] = NewInt(count) + } + return NewHash(result), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown array method %s", property) + } +} diff --git a/vibes/execution_members_duration.go b/vibes/execution_members_duration.go new file mode 100644 index 0000000..f06e4fd --- /dev/null +++ b/vibes/execution_members_duration.go @@ -0,0 +1,103 @@ +package vibes + +import ( + "fmt" + "time" +) + +func durationMember(d Duration, property string, pos Position) (Value, error) { + switch property { + case "seconds", "second": + return NewInt(d.Seconds()), nil + case "minutes", "minute": + return NewInt(d.Seconds() / 60), nil + case "hours", "hour": + return NewInt(d.Seconds() / 3600), nil + case "days", "day": + return NewInt(d.Seconds() / 86400), nil + case "weeks", "week": + return NewInt(d.Seconds() / 604800), nil + case "in_seconds": + return NewFloat(float64(d.Seconds())), nil + case "in_minutes": + return NewFloat(float64(d.Seconds()) / 60), nil + case "in_hours": + return NewFloat(float64(d.Seconds()) / 3600), nil + case "in_days": + return NewFloat(float64(d.Seconds()) / 86400), nil + case "in_weeks": + return NewFloat(float64(d.Seconds()) / 604800), nil + case "in_months": + return NewFloat(float64(d.Seconds()) / (30 * 86400)), nil + case "in_years": + return NewFloat(float64(d.Seconds()) / (365 * 86400)), nil + case "iso8601": + return NewString(d.iso8601()), nil + case "parts": + p := d.parts() + return NewHash(map[string]Value{ + "days": NewInt(p["days"]), + "hours": NewInt(p["hours"]), + "minutes": NewInt(p["minutes"]), + "seconds": NewInt(p["seconds"]), + }), nil + case "to_i": + return NewInt(d.Seconds()), nil + case "to_s": + return NewString(d.String()), nil + case "format": + return NewString(d.String()), nil + case "eql?": + return NewBuiltin("duration.eql?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 || args[0].Kind() != KindDuration { + return NewNil(), fmt.Errorf("duration.eql? expects a duration") + } + return NewBool(d.Seconds() == args[0].Duration().Seconds()), nil + }), nil + case "after", "since", "from_now": + return NewBuiltin("duration.after", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + start, err := durationTimeArg(args, true, "after") + if err != nil { + return NewNil(), err + } + result := start.Add(time.Duration(d.Seconds()) * time.Second).UTC() + return NewTime(result), nil + }), nil + case "ago", "before", "until": + return NewBuiltin("duration.before", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + start, err := durationTimeArg(args, true, "before") + if err != nil { + return NewNil(), err + } + result := start.Add(-time.Duration(d.Seconds()) * time.Second).UTC() + return NewTime(result), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown duration method %s", property) + } +} + +func durationTimeArg(args []Value, allowEmpty bool, name string) (time.Time, error) { + if len(args) == 0 { + if allowEmpty { + return time.Now().UTC(), nil + } + return time.Time{}, fmt.Errorf("%s expects a time argument", name) + } + if len(args) != 1 { + return time.Time{}, fmt.Errorf("%s expects at most one time argument", name) + } + val := args[0] + switch val.Kind() { + case KindString: + t, err := time.Parse(time.RFC3339, val.String()) + if err != nil { + return time.Time{}, fmt.Errorf("invalid time: %v", err) + } + return t.UTC(), nil + case KindTime: + return val.Time(), nil + default: + return time.Time{}, fmt.Errorf("%s expects a Time or RFC3339 string", name) + } +} diff --git a/vibes/execution_members_hash.go b/vibes/execution_members_hash.go new file mode 100644 index 0000000..df0f0ef --- /dev/null +++ b/vibes/execution_members_hash.go @@ -0,0 +1,411 @@ +package vibes + +import ( + "fmt" + "maps" + "reflect" + "sort" +) + +func hashMember(obj Value, property string) (Value, error) { + switch property { + case "size": + return NewAutoBuiltin("hash.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.size does not take arguments") + } + return NewInt(int64(len(receiver.Hash()))), nil + }), nil + case "length": + return NewAutoBuiltin("hash.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.length does not take arguments") + } + return NewInt(int64(len(receiver.Hash()))), nil + }), nil + case "empty?": + return NewAutoBuiltin("hash.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.empty? does not take arguments") + } + return NewBool(len(receiver.Hash()) == 0), nil + }), nil + case "key?", "has_key?", "include?": + name := property + return NewAutoBuiltin("hash."+name, func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("hash.%s expects exactly one key", name) + } + key, err := valueToHashKey(args[0]) + if err != nil { + return NewNil(), fmt.Errorf("hash.%s key must be symbol or string", name) + } + _, ok := receiver.Hash()[key] + return NewBool(ok), nil + }), nil + case "keys": + return NewAutoBuiltin("hash.keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.keys does not take arguments") + } + keys := sortedHashKeys(receiver.Hash()) + values := make([]Value, len(keys)) + for i, k := range keys { + values[i] = NewSymbol(k) + } + return NewArray(values), nil + }), nil + case "values": + return NewAutoBuiltin("hash.values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.values does not take arguments") + } + entries := receiver.Hash() + keys := sortedHashKeys(entries) + values := make([]Value, len(keys)) + for i, k := range keys { + values[i] = entries[k] + } + return NewArray(values), nil + }), nil + case "fetch": + return NewAutoBuiltin("hash.fetch", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("hash.fetch expects key and optional default") + } + key, err := valueToHashKey(args[0]) + if err != nil { + return NewNil(), fmt.Errorf("hash.fetch key must be symbol or string") + } + if value, ok := receiver.Hash()[key]; ok { + return value, nil + } + if len(args) == 2 { + return args[1], nil + } + return NewNil(), nil + }), nil + case "dig": + return NewAutoBuiltin("hash.dig", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) == 0 { + return NewNil(), fmt.Errorf("hash.dig expects at least one key") + } + current := receiver + for _, arg := range args { + key, err := valueToHashKey(arg) + if err != nil { + return NewNil(), fmt.Errorf("hash.dig path keys must be symbol or string") + } + if current.Kind() != KindHash && current.Kind() != KindObject { + return NewNil(), nil + } + next, ok := current.Hash()[key] + if !ok { + return NewNil(), nil + } + current = next + } + return current, nil + }), nil + case "each": + return NewAutoBuiltin("hash.each", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.each does not take arguments") + } + if err := ensureBlock(block, "hash.each"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + for _, key := range sortedHashKeys(entries) { + if _, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "each_key": + return NewAutoBuiltin("hash.each_key", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.each_key does not take arguments") + } + if err := ensureBlock(block, "hash.each_key"); err != nil { + return NewNil(), err + } + for _, key := range sortedHashKeys(receiver.Hash()) { + if _, err := exec.CallBlock(block, []Value{NewSymbol(key)}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "each_value": + return NewAutoBuiltin("hash.each_value", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.each_value does not take arguments") + } + if err := ensureBlock(block, "hash.each_value"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + for _, key := range sortedHashKeys(entries) { + if _, err := exec.CallBlock(block, []Value{entries[key]}); err != nil { + return NewNil(), err + } + } + return receiver, nil + }), nil + case "merge": + return NewBuiltin("hash.merge", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { + return NewNil(), fmt.Errorf("hash.merge expects a single hash argument") + } + base := receiver.Hash() + addition := args[0].Hash() + out := make(map[string]Value, len(base)+len(addition)) + maps.Copy(out, base) + maps.Copy(out, addition) + return NewHash(out), nil + }), nil + case "slice": + return NewBuiltin("hash.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + entries := receiver.Hash() + out := make(map[string]Value, len(args)) + for _, arg := range args { + key, err := valueToHashKey(arg) + if err != nil { + return NewNil(), fmt.Errorf("hash.slice keys must be symbol or string") + } + if value, ok := entries[key]; ok { + out[key] = value + } + } + return NewHash(out), nil + }), nil + case "except": + return NewBuiltin("hash.except", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + excluded := make(map[string]struct{}, len(args)) + for _, arg := range args { + key, err := valueToHashKey(arg) + if err != nil { + return NewNil(), fmt.Errorf("hash.except keys must be symbol or string") + } + excluded[key] = struct{}{} + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for key, value := range entries { + if _, skip := excluded[key]; skip { + continue + } + out[key] = value + } + return NewHash(out), nil + }), nil + case "select": + return NewAutoBuiltin("hash.select", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.select does not take arguments") + } + if err := ensureBlock(block, "hash.select"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + include, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) + if err != nil { + return NewNil(), err + } + if include.Truthy() { + out[key] = entries[key] + } + } + return NewHash(out), nil + }), nil + case "reject": + return NewAutoBuiltin("hash.reject", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.reject does not take arguments") + } + if err := ensureBlock(block, "hash.reject"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + exclude, err := exec.CallBlock(block, []Value{NewSymbol(key), entries[key]}) + if err != nil { + return NewNil(), err + } + if !exclude.Truthy() { + out[key] = entries[key] + } + } + return NewHash(out), nil + }), nil + case "transform_keys": + return NewAutoBuiltin("hash.transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.transform_keys does not take arguments") + } + if err := ensureBlock(block, "hash.transform_keys"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + nextKey, err := exec.CallBlock(block, []Value{NewSymbol(key)}) + if err != nil { + return NewNil(), err + } + resolved, err := valueToHashKey(nextKey) + if err != nil { + return NewNil(), fmt.Errorf("hash.transform_keys block must return symbol or string") + } + out[resolved] = entries[key] + } + return NewHash(out), nil + }), nil + case "deep_transform_keys": + return NewAutoBuiltin("hash.deep_transform_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.deep_transform_keys does not take arguments") + } + if err := ensureBlock(block, "hash.deep_transform_keys"); err != nil { + return NewNil(), err + } + return deepTransformKeys(exec, receiver, block) + }), nil + case "remap_keys": + return NewBuiltin("hash.remap_keys", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 || (args[0].Kind() != KindHash && args[0].Kind() != KindObject) { + return NewNil(), fmt.Errorf("hash.remap_keys expects a key mapping hash") + } + entries := receiver.Hash() + mapping := args[0].Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + value := entries[key] + if mapped, ok := mapping[key]; ok { + nextKey, err := valueToHashKey(mapped) + if err != nil { + return NewNil(), fmt.Errorf("hash.remap_keys mapping values must be symbol or string") + } + out[nextKey] = value + continue + } + out[key] = value + } + return NewHash(out), nil + }), nil + case "transform_values": + return NewAutoBuiltin("hash.transform_values", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.transform_values does not take arguments") + } + if err := ensureBlock(block, "hash.transform_values"); err != nil { + return NewNil(), err + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + nextValue, err := exec.CallBlock(block, []Value{entries[key]}) + if err != nil { + return NewNil(), err + } + out[key] = nextValue + } + return NewHash(out), nil + }), nil + case "compact": + return NewAutoBuiltin("hash.compact", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("hash.compact does not take arguments") + } + entries := receiver.Hash() + out := make(map[string]Value, len(entries)) + for k, v := range entries { + if v.Kind() != KindNil { + out[k] = v + } + } + return NewHash(out), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown hash method %s", property) + } +} + +func sortedHashKeys(entries map[string]Value) []string { + keys := make([]string, 0, len(entries)) + for key := range entries { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func deepTransformKeys(exec *Execution, value Value, block Value) (Value, error) { + return deepTransformKeysWithState(exec, value, block, &deepTransformState{ + seenHashes: make(map[uintptr]struct{}), + seenArrays: make(map[uintptr]struct{}), + }) +} + +type deepTransformState struct { + seenHashes map[uintptr]struct{} + seenArrays map[uintptr]struct{} +} + +func deepTransformKeysWithState(exec *Execution, value Value, block Value, state *deepTransformState) (Value, error) { + switch value.Kind() { + case KindHash, KindObject: + entries := value.Hash() + id := reflect.ValueOf(entries).Pointer() + if id != 0 { + if _, seen := state.seenHashes[id]; seen { + return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") + } + state.seenHashes[id] = struct{}{} + defer delete(state.seenHashes, id) + } + out := make(map[string]Value, len(entries)) + for _, key := range sortedHashKeys(entries) { + nextKeyValue, err := exec.CallBlock(block, []Value{NewSymbol(key)}) + if err != nil { + return NewNil(), err + } + nextKey, err := valueToHashKey(nextKeyValue) + if err != nil { + return NewNil(), fmt.Errorf("hash.deep_transform_keys block must return symbol or string") + } + nextValue, err := deepTransformKeysWithState(exec, entries[key], block, state) + if err != nil { + return NewNil(), err + } + out[nextKey] = nextValue + } + return NewHash(out), nil + case KindArray: + items := value.Array() + id := reflect.ValueOf(items).Pointer() + if id != 0 { + if _, seen := state.seenArrays[id]; seen { + return NewNil(), fmt.Errorf("hash.deep_transform_keys does not support cyclic structures") + } + state.seenArrays[id] = struct{}{} + defer delete(state.seenArrays, id) + } + out := make([]Value, len(items)) + for i, item := range items { + nextValue, err := deepTransformKeysWithState(exec, item, block, state) + if err != nil { + return NewNil(), err + } + out[i] = nextValue + } + return NewArray(out), nil + default: + return value, nil + } +} diff --git a/vibes/execution_members_string.go b/vibes/execution_members_string.go new file mode 100644 index 0000000..99385fd --- /dev/null +++ b/vibes/execution_members_string.go @@ -0,0 +1,839 @@ +package vibes + +import ( + "fmt" + "regexp" + "strings" + "unicode" +) + +func chompDefault(text string) string { + if strings.HasSuffix(text, "\r\n") { + return text[:len(text)-2] + } + if strings.HasSuffix(text, "\n") || strings.HasSuffix(text, "\r") { + return text[:len(text)-1] + } + return text +} + +func stringRuneIndex(text, needle string, offset int) int { + hayRunes := []rune(text) + needleRunes := []rune(needle) + if offset < 0 || offset > len(hayRunes) { + return -1 + } + if len(needleRunes) == 0 { + return offset + } + limit := len(hayRunes) - len(needleRunes) + if limit < offset { + return -1 + } + for i := offset; i <= limit; i++ { + match := true + for j := range len(needleRunes) { + if hayRunes[i+j] != needleRunes[j] { + match = false + break + } + } + if match { + return i + } + } + return -1 +} + +func stringRuneRIndex(text, needle string, offset int) int { + hayRunes := []rune(text) + needleRunes := []rune(needle) + if offset < 0 { + return -1 + } + if offset > len(hayRunes) { + offset = len(hayRunes) + } + if len(needleRunes) == 0 { + return offset + } + if len(needleRunes) > len(hayRunes) { + return -1 + } + start := offset + maxStart := len(hayRunes) - len(needleRunes) + if start > maxStart { + start = maxStart + } + for i := start; i >= 0; i-- { + match := true + for j := range len(needleRunes) { + if hayRunes[i+j] != needleRunes[j] { + match = false + break + } + } + if match { + return i + } + } + return -1 +} + +func stringRuneSlice(text string, start, length int) (string, bool) { + runes := []rune(text) + if start < 0 || start >= len(runes) { + return "", false + } + if length < 0 { + return "", false + } + remaining := len(runes) - start + if length >= remaining { + return string(runes[start:]), true + } + end := start + length + return string(runes[start:end]), true +} + +func stringCapitalize(text string) string { + runes := []rune(text) + if len(runes) == 0 { + return "" + } + runes[0] = unicode.ToUpper(runes[0]) + for i := 1; i < len(runes); i++ { + runes[i] = unicode.ToLower(runes[i]) + } + return string(runes) +} + +func stringSwapCase(text string) string { + runes := []rune(text) + for i, r := range runes { + if unicode.IsUpper(r) { + runes[i] = unicode.ToLower(r) + continue + } + if unicode.IsLower(r) { + runes[i] = unicode.ToUpper(r) + } + } + return string(runes) +} + +func stringReverse(text string) string { + runes := []rune(text) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} + +func stringRegexOption(method string, kwargs map[string]Value) (bool, error) { + if len(kwargs) == 0 { + return false, nil + } + regexVal, ok := kwargs["regex"] + if !ok || len(kwargs) > 1 { + return false, fmt.Errorf("string.%s supports only regex keyword", method) + } + if regexVal.Kind() != KindBool { + return false, fmt.Errorf("string.%s regex keyword must be bool", method) + } + return regexVal.Bool(), nil +} + +func stringSub(text, pattern, replacement string, regex bool) (string, error) { + if !regex { + return strings.Replace(text, pattern, replacement, 1), nil + } + re, err := regexp.Compile(pattern) + if err != nil { + return "", err + } + loc := re.FindStringSubmatchIndex(text) + if loc == nil { + return text, nil + } + replaced := re.ExpandString(nil, replacement, text, loc) + return text[:loc[0]] + string(replaced) + text[loc[1]:], nil +} + +func stringGSub(text, pattern, replacement string, regex bool) (string, error) { + if !regex { + return strings.ReplaceAll(text, pattern, replacement), nil + } + re, err := regexp.Compile(pattern) + if err != nil { + return "", err + } + return re.ReplaceAllString(text, replacement), nil +} + +func stringBangResult(original, updated string) Value { + if updated == original { + return NewNil() + } + return NewString(updated) +} + +func stringSquish(text string) string { + return strings.Join(strings.Fields(text), " ") +} + +func stringTemplateOption(kwargs map[string]Value) (bool, error) { + if len(kwargs) == 0 { + return false, nil + } + value, ok := kwargs["strict"] + if !ok || len(kwargs) != 1 { + return false, fmt.Errorf("string.template supports only strict keyword") + } + if value.Kind() != KindBool { + return false, fmt.Errorf("string.template strict keyword must be bool") + } + return value.Bool(), nil +} + +func stringTemplateLookup(context Value, keyPath string) (Value, bool) { + current := context + for _, segment := range strings.Split(keyPath, ".") { + if segment == "" { + return NewNil(), false + } + if current.Kind() != KindHash && current.Kind() != KindObject { + return NewNil(), false + } + next, ok := current.Hash()[segment] + if !ok { + return NewNil(), false + } + current = next + } + return current, true +} + +func stringTemplateScalarValue(value Value, keyPath string) (string, error) { + switch value.Kind() { + case KindNil, KindBool, KindInt, KindFloat, KindString, KindSymbol, KindMoney, KindDuration, KindTime: + return value.String(), nil + default: + return "", fmt.Errorf("string.template placeholder %s value must be scalar", keyPath) + } +} + +func stringTemplate(text string, context Value, strict bool) (string, error) { + templateErr := error(nil) + rendered := stringTemplatePattern.ReplaceAllStringFunc(text, func(match string) string { + if templateErr != nil { + return match + } + submatch := stringTemplatePattern.FindStringSubmatch(match) + if len(submatch) != 2 { + return match + } + keyPath := submatch[1] + value, ok := stringTemplateLookup(context, keyPath) + if !ok { + if strict { + templateErr = fmt.Errorf("string.template missing placeholder %s", keyPath) + } + return match + } + segment, err := stringTemplateScalarValue(value, keyPath) + if err != nil { + templateErr = err + return match + } + return segment + }) + if templateErr != nil { + return "", templateErr + } + return rendered, nil +} + +func stringMember(str Value, property string) (Value, error) { + switch property { + case "size": + return NewAutoBuiltin("string.size", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.size does not take arguments") + } + return NewInt(int64(len([]rune(receiver.String())))), nil + }), nil + case "length": + return NewAutoBuiltin("string.length", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.length does not take arguments") + } + return NewInt(int64(len([]rune(receiver.String())))), nil + }), nil + case "bytesize": + return NewAutoBuiltin("string.bytesize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.bytesize does not take arguments") + } + return NewInt(int64(len(receiver.String()))), nil + }), nil + case "ord": + return NewAutoBuiltin("string.ord", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.ord does not take arguments") + } + runes := []rune(receiver.String()) + if len(runes) == 0 { + return NewNil(), fmt.Errorf("string.ord requires non-empty string") + } + return NewInt(int64(runes[0])), nil + }), nil + case "chr": + return NewAutoBuiltin("string.chr", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.chr does not take arguments") + } + runes := []rune(receiver.String()) + if len(runes) == 0 { + return NewNil(), nil + } + return NewString(string(runes[0])), nil + }), nil + case "empty?": + return NewAutoBuiltin("string.empty?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.empty? does not take arguments") + } + return NewBool(len(receiver.String()) == 0), nil + }), nil + case "clear": + return NewAutoBuiltin("string.clear", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.clear does not take arguments") + } + return NewString(""), nil + }), nil + case "concat": + return NewAutoBuiltin("string.concat", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + var b strings.Builder + b.WriteString(receiver.String()) + for _, arg := range args { + if arg.Kind() != KindString { + return NewNil(), fmt.Errorf("string.concat expects string arguments") + } + b.WriteString(arg.String()) + } + return NewString(b.String()), nil + }), nil + case "replace": + return NewAutoBuiltin("string.replace", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.replace expects exactly one replacement") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.replace replacement must be string") + } + return NewString(args[0].String()), nil + }), nil + case "start_with?": + return NewAutoBuiltin("string.start_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.start_with? expects exactly one prefix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.start_with? prefix must be string") + } + return NewBool(strings.HasPrefix(receiver.String(), args[0].String())), nil + }), nil + case "end_with?": + return NewAutoBuiltin("string.end_with?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.end_with? expects exactly one suffix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.end_with? suffix must be string") + } + return NewBool(strings.HasSuffix(receiver.String(), args[0].String())), nil + }), nil + case "include?": + return NewAutoBuiltin("string.include?", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.include? expects exactly one substring") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.include? substring must be string") + } + return NewBool(strings.Contains(receiver.String(), args[0].String())), nil + }), nil + case "match": + return NewAutoBuiltin("string.match", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("string.match does not take keyword arguments") + } + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.match expects exactly one pattern") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.match pattern must be string") + } + pattern := args[0].String() + re, err := regexp.Compile(pattern) + if err != nil { + return NewNil(), fmt.Errorf("string.match invalid regex: %v", err) + } + text := receiver.String() + indices := re.FindStringSubmatchIndex(text) + if indices == nil { + return NewNil(), nil + } + values := make([]Value, len(indices)/2) + for i := range values { + start := indices[i*2] + end := indices[i*2+1] + if start < 0 || end < 0 { + values[i] = NewNil() + continue + } + values[i] = NewString(text[start:end]) + } + return NewArray(values), nil + }), nil + case "scan": + return NewAutoBuiltin("string.scan", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(kwargs) > 0 { + return NewNil(), fmt.Errorf("string.scan does not take keyword arguments") + } + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.scan expects exactly one pattern") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.scan pattern must be string") + } + pattern := args[0].String() + re, err := regexp.Compile(pattern) + if err != nil { + return NewNil(), fmt.Errorf("string.scan invalid regex: %v", err) + } + matches := re.FindAllString(receiver.String(), -1) + values := make([]Value, len(matches)) + for i, m := range matches { + values[i] = NewString(m) + } + return NewArray(values), nil + }), nil + case "index": + return NewAutoBuiltin("string.index", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("string.index expects substring and optional offset") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.index substring must be string") + } + offset := 0 + if len(args) == 2 { + i, err := valueToInt(args[1]) + if err != nil || i < 0 { + return NewNil(), fmt.Errorf("string.index offset must be non-negative integer") + } + offset = i + } + index := stringRuneIndex(receiver.String(), args[0].String(), offset) + if index < 0 { + return NewNil(), nil + } + return NewInt(int64(index)), nil + }), nil + case "rindex": + return NewAutoBuiltin("string.rindex", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("string.rindex expects substring and optional offset") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.rindex substring must be string") + } + offset := len([]rune(receiver.String())) + if len(args) == 2 { + i, err := valueToInt(args[1]) + if err != nil || i < 0 { + return NewNil(), fmt.Errorf("string.rindex offset must be non-negative integer") + } + offset = i + } + index := stringRuneRIndex(receiver.String(), args[0].String(), offset) + if index < 0 { + return NewNil(), nil + } + return NewInt(int64(index)), nil + }), nil + case "slice": + return NewAutoBuiltin("string.slice", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("string.slice expects index and optional length") + } + start, err := valueToInt(args[0]) + if err != nil { + return NewNil(), fmt.Errorf("string.slice index must be integer") + } + runes := []rune(receiver.String()) + if len(args) == 1 { + if start < 0 || start >= len(runes) { + return NewNil(), nil + } + return NewString(string(runes[start])), nil + } + length, err := valueToInt(args[1]) + if err != nil { + return NewNil(), fmt.Errorf("string.slice length must be integer") + } + substr, ok := stringRuneSlice(receiver.String(), start, length) + if !ok { + return NewNil(), nil + } + return NewString(substr), nil + }), nil + case "strip": + return NewAutoBuiltin("string.strip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.strip does not take arguments") + } + return NewString(strings.TrimSpace(receiver.String())), nil + }), nil + case "strip!": + return NewAutoBuiltin("string.strip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.strip! does not take arguments") + } + updated := strings.TrimSpace(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "squish": + return NewAutoBuiltin("string.squish", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.squish does not take arguments") + } + return NewString(stringSquish(receiver.String())), nil + }), nil + case "squish!": + return NewAutoBuiltin("string.squish!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.squish! does not take arguments") + } + updated := stringSquish(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "lstrip": + return NewAutoBuiltin("string.lstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.lstrip does not take arguments") + } + return NewString(strings.TrimLeftFunc(receiver.String(), unicode.IsSpace)), nil + }), nil + case "lstrip!": + return NewAutoBuiltin("string.lstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.lstrip! does not take arguments") + } + updated := strings.TrimLeftFunc(receiver.String(), unicode.IsSpace) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "rstrip": + return NewAutoBuiltin("string.rstrip", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.rstrip does not take arguments") + } + return NewString(strings.TrimRightFunc(receiver.String(), unicode.IsSpace)), nil + }), nil + case "rstrip!": + return NewAutoBuiltin("string.rstrip!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.rstrip! does not take arguments") + } + updated := strings.TrimRightFunc(receiver.String(), unicode.IsSpace) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "chomp": + return NewAutoBuiltin("string.chomp", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("string.chomp accepts at most one separator") + } + text := receiver.String() + if len(args) == 0 { + return NewString(chompDefault(text)), nil + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.chomp separator must be string") + } + sep := args[0].String() + if sep == "" { + return NewString(strings.TrimRight(text, "\r\n")), nil + } + if strings.HasSuffix(text, sep) { + return NewString(text[:len(text)-len(sep)]), nil + } + return NewString(text), nil + }), nil + case "chomp!": + return NewAutoBuiltin("string.chomp!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("string.chomp! accepts at most one separator") + } + original := receiver.String() + if len(args) == 0 { + return stringBangResult(original, chompDefault(original)), nil + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.chomp! separator must be string") + } + sep := args[0].String() + if sep == "" { + return stringBangResult(original, strings.TrimRight(original, "\r\n")), nil + } + if strings.HasSuffix(original, sep) { + return stringBangResult(original, original[:len(original)-len(sep)]), nil + } + return NewNil(), nil + }), nil + case "delete_prefix": + return NewAutoBuiltin("string.delete_prefix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_prefix expects exactly one prefix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_prefix prefix must be string") + } + return NewString(strings.TrimPrefix(receiver.String(), args[0].String())), nil + }), nil + case "delete_prefix!": + return NewAutoBuiltin("string.delete_prefix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_prefix! expects exactly one prefix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_prefix! prefix must be string") + } + updated := strings.TrimPrefix(receiver.String(), args[0].String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "delete_suffix": + return NewAutoBuiltin("string.delete_suffix", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_suffix expects exactly one suffix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_suffix suffix must be string") + } + return NewString(strings.TrimSuffix(receiver.String(), args[0].String())), nil + }), nil + case "delete_suffix!": + return NewAutoBuiltin("string.delete_suffix!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.delete_suffix! expects exactly one suffix") + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.delete_suffix! suffix must be string") + } + updated := strings.TrimSuffix(receiver.String(), args[0].String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "upcase": + return NewAutoBuiltin("string.upcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.upcase does not take arguments") + } + return NewString(strings.ToUpper(receiver.String())), nil + }), nil + case "upcase!": + return NewAutoBuiltin("string.upcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.upcase! does not take arguments") + } + updated := strings.ToUpper(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "downcase": + return NewAutoBuiltin("string.downcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.downcase does not take arguments") + } + return NewString(strings.ToLower(receiver.String())), nil + }), nil + case "downcase!": + return NewAutoBuiltin("string.downcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.downcase! does not take arguments") + } + updated := strings.ToLower(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "capitalize": + return NewAutoBuiltin("string.capitalize", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.capitalize does not take arguments") + } + return NewString(stringCapitalize(receiver.String())), nil + }), nil + case "capitalize!": + return NewAutoBuiltin("string.capitalize!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.capitalize! does not take arguments") + } + updated := stringCapitalize(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "swapcase": + return NewAutoBuiltin("string.swapcase", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.swapcase does not take arguments") + } + return NewString(stringSwapCase(receiver.String())), nil + }), nil + case "swapcase!": + return NewAutoBuiltin("string.swapcase!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.swapcase! does not take arguments") + } + updated := stringSwapCase(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "reverse": + return NewAutoBuiltin("string.reverse", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.reverse does not take arguments") + } + return NewString(stringReverse(receiver.String())), nil + }), nil + case "reverse!": + return NewAutoBuiltin("string.reverse!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 0 { + return NewNil(), fmt.Errorf("string.reverse! does not take arguments") + } + updated := stringReverse(receiver.String()) + return stringBangResult(receiver.String(), updated), nil + }), nil + case "sub": + return NewAutoBuiltin("string.sub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.sub expects pattern and replacement") + } + regex, err := stringRegexOption("sub", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub replacement must be string") + } + updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.sub invalid regex: %v", err) + } + return NewString(updated), nil + }), nil + case "sub!": + return NewAutoBuiltin("string.sub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.sub! expects pattern and replacement") + } + regex, err := stringRegexOption("sub!", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub! pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.sub! replacement must be string") + } + updated, err := stringSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.sub! invalid regex: %v", err) + } + return stringBangResult(receiver.String(), updated), nil + }), nil + case "gsub": + return NewAutoBuiltin("string.gsub", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.gsub expects pattern and replacement") + } + regex, err := stringRegexOption("gsub", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub replacement must be string") + } + updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.gsub invalid regex: %v", err) + } + return NewString(updated), nil + }), nil + case "gsub!": + return NewAutoBuiltin("string.gsub!", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("string.gsub! expects pattern and replacement") + } + regex, err := stringRegexOption("gsub!", kwargs) + if err != nil { + return NewNil(), err + } + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub! pattern must be string") + } + if args[1].Kind() != KindString { + return NewNil(), fmt.Errorf("string.gsub! replacement must be string") + } + updated, err := stringGSub(receiver.String(), args[0].String(), args[1].String(), regex) + if err != nil { + return NewNil(), fmt.Errorf("string.gsub! invalid regex: %v", err) + } + return stringBangResult(receiver.String(), updated), nil + }), nil + case "split": + return NewAutoBuiltin("string.split", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) > 1 { + return NewNil(), fmt.Errorf("string.split accepts at most one separator") + } + text := receiver.String() + var parts []string + if len(args) == 0 { + parts = strings.Fields(text) + } else { + if args[0].Kind() != KindString { + return NewNil(), fmt.Errorf("string.split separator must be string") + } + parts = strings.Split(text, args[0].String()) + } + values := make([]Value, len(parts)) + for i, part := range parts { + values[i] = NewString(part) + } + return NewArray(values), nil + }), nil + case "template": + return NewAutoBuiltin("string.template", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 1 { + return NewNil(), fmt.Errorf("string.template expects exactly one context hash") + } + if args[0].Kind() != KindHash && args[0].Kind() != KindObject { + return NewNil(), fmt.Errorf("string.template context must be hash") + } + strict, err := stringTemplateOption(kwargs) + if err != nil { + return NewNil(), err + } + rendered, err := stringTemplate(receiver.String(), args[0], strict) + if err != nil { + return NewNil(), err + } + return NewString(rendered), nil + }), nil + default: + return NewNil(), fmt.Errorf("unknown string method %s", property) + } +} From 44e68a0df8e05029de7b482853c068a839547699 Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:13:20 -0500 Subject: [PATCH 12/99] update architecture docs for refactored interpreter layout --- docs/architecture.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 452055b..ac01ae6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,7 +17,14 @@ High-level call path: Key files: -- `vibes/execution.go` (core evaluator, call orchestration) +- `vibes/execution.go` (core statement/expression evaluator) +- `vibes/execution_script.go` (script call surface and function argument binding) +- `vibes/execution_control.go` (range/case/loop/try evaluation) +- `vibes/execution_members.go` (member dispatch for runtime values) +- `vibes/execution_members_hash.go` (hash/object member behavior) +- `vibes/execution_members_string.go` (string member behavior) +- `vibes/execution_members_duration.go` (duration member behavior) +- `vibes/execution_members_array.go` (array member behavior) - `vibes/execution_types.go` (type-checking + type formatting helpers) - `vibes/execution_values.go` (value conversion, arithmetic, comparison helpers) @@ -33,6 +40,7 @@ Key files: - `vibes/lexer.go` - `vibes/parser.go` (parser core + precedence + token/error helpers) +- `vibes/parser_expressions.go` (expression-level parsing, call/block literals) - `vibes/parser_statements.go` (statement-level parsing) - `vibes/parser_types.go` (type-expression parsing) - `vibes/ast.go` From 4fee204d54f81c8d6b7fc247959e5e345586031d Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:15:42 -0500 Subject: [PATCH 13/99] extract execution state stack helpers from evaluator core --- vibes/execution.go | 93 --------------------------------------- vibes/execution_state.go | 94 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 93 deletions(-) create mode 100644 vibes/execution_state.go diff --git a/vibes/execution.go b/vibes/execution.go index 9bcf38e..b4d7e44 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -257,99 +257,6 @@ func (exec *Execution) wrapError(err error, pos Position) error { return exec.newRuntimeErrorWithType(classifyRuntimeErrorType(err), err.Error(), pos) } -func (exec *Execution) pushReceiver(v Value) { - exec.receiverStack = append(exec.receiverStack, v) -} - -func (exec *Execution) popReceiver() { - if len(exec.receiverStack) == 0 { - return - } - exec.receiverStack = exec.receiverStack[:len(exec.receiverStack)-1] -} - -func (exec *Execution) currentReceiver() Value { - if len(exec.receiverStack) == 0 { - return NewNil() - } - return exec.receiverStack[len(exec.receiverStack)-1] -} - -func (exec *Execution) isCurrentReceiver(v Value) bool { - cur := exec.currentReceiver() - switch { - case v.Kind() == KindInstance && cur.Kind() == KindInstance: - return v.Instance() == cur.Instance() - case v.Kind() == KindClass && cur.Kind() == KindClass: - return v.Class() == cur.Class() - default: - return false - } -} - -func (exec *Execution) pushFrame(function string, pos Position) error { - if exec.recursionCap > 0 && len(exec.callStack) >= exec.recursionCap { - return exec.errorAt(pos, "recursion depth exceeded (limit %d)", exec.recursionCap) - } - exec.callStack = append(exec.callStack, callFrame{Function: function, Pos: pos}) - return nil -} - -func (exec *Execution) popFrame() { - if len(exec.callStack) == 0 { - return - } - exec.callStack = exec.callStack[:len(exec.callStack)-1] -} - -func (exec *Execution) pushEnv(env *Env) { - exec.envStack = append(exec.envStack, env) -} - -func (exec *Execution) popEnv() { - if len(exec.envStack) == 0 { - return - } - exec.envStack = exec.envStack[:len(exec.envStack)-1] -} - -func (exec *Execution) pushModuleContext(ctx moduleContext) { - exec.moduleStack = append(exec.moduleStack, ctx) -} - -func (exec *Execution) popModuleContext() { - if len(exec.moduleStack) == 0 { - return - } - exec.moduleStack = exec.moduleStack[:len(exec.moduleStack)-1] -} - -func (exec *Execution) currentModuleContext() *moduleContext { - if len(exec.moduleStack) == 0 { - return nil - } - ctx := exec.moduleStack[len(exec.moduleStack)-1] - return &ctx -} - -func (exec *Execution) pushRescuedError(err error) { - exec.rescuedErrors = append(exec.rescuedErrors, err) -} - -func (exec *Execution) popRescuedError() { - if len(exec.rescuedErrors) == 0 { - return - } - exec.rescuedErrors = exec.rescuedErrors[:len(exec.rescuedErrors)-1] -} - -func (exec *Execution) currentRescuedError() error { - if len(exec.rescuedErrors) == 0 { - return nil - } - return exec.rescuedErrors[len(exec.rescuedErrors)-1] -} - func (exec *Execution) evalStatements(stmts []Statement, env *Env) (Value, bool, error) { exec.pushEnv(env) defer exec.popEnv() diff --git a/vibes/execution_state.go b/vibes/execution_state.go new file mode 100644 index 0000000..9524c66 --- /dev/null +++ b/vibes/execution_state.go @@ -0,0 +1,94 @@ +package vibes + +func (exec *Execution) pushReceiver(v Value) { + exec.receiverStack = append(exec.receiverStack, v) +} + +func (exec *Execution) popReceiver() { + if len(exec.receiverStack) == 0 { + return + } + exec.receiverStack = exec.receiverStack[:len(exec.receiverStack)-1] +} + +func (exec *Execution) currentReceiver() Value { + if len(exec.receiverStack) == 0 { + return NewNil() + } + return exec.receiverStack[len(exec.receiverStack)-1] +} + +func (exec *Execution) isCurrentReceiver(v Value) bool { + cur := exec.currentReceiver() + switch { + case v.Kind() == KindInstance && cur.Kind() == KindInstance: + return v.Instance() == cur.Instance() + case v.Kind() == KindClass && cur.Kind() == KindClass: + return v.Class() == cur.Class() + default: + return false + } +} + +func (exec *Execution) pushFrame(function string, pos Position) error { + if exec.recursionCap > 0 && len(exec.callStack) >= exec.recursionCap { + return exec.errorAt(pos, "recursion depth exceeded (limit %d)", exec.recursionCap) + } + exec.callStack = append(exec.callStack, callFrame{Function: function, Pos: pos}) + return nil +} + +func (exec *Execution) popFrame() { + if len(exec.callStack) == 0 { + return + } + exec.callStack = exec.callStack[:len(exec.callStack)-1] +} + +func (exec *Execution) pushEnv(env *Env) { + exec.envStack = append(exec.envStack, env) +} + +func (exec *Execution) popEnv() { + if len(exec.envStack) == 0 { + return + } + exec.envStack = exec.envStack[:len(exec.envStack)-1] +} + +func (exec *Execution) pushModuleContext(ctx moduleContext) { + exec.moduleStack = append(exec.moduleStack, ctx) +} + +func (exec *Execution) popModuleContext() { + if len(exec.moduleStack) == 0 { + return + } + exec.moduleStack = exec.moduleStack[:len(exec.moduleStack)-1] +} + +func (exec *Execution) currentModuleContext() *moduleContext { + if len(exec.moduleStack) == 0 { + return nil + } + ctx := exec.moduleStack[len(exec.moduleStack)-1] + return &ctx +} + +func (exec *Execution) pushRescuedError(err error) { + exec.rescuedErrors = append(exec.rescuedErrors, err) +} + +func (exec *Execution) popRescuedError() { + if len(exec.rescuedErrors) == 0 { + return + } + exec.rescuedErrors = exec.rescuedErrors[:len(exec.rescuedErrors)-1] +} + +func (exec *Execution) currentRescuedError() error { + if len(exec.rescuedErrors) == 0 { + return nil + } + return exec.rescuedErrors[len(exec.rescuedErrors)-1] +} From 014e90cb13de5a9677011093554a175b616cec5e Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:16:05 -0500 Subject: [PATCH 14/99] extract call and block invocation flow from execution core --- vibes/execution.go | 476 -------------------------------------- vibes/execution_calls.go | 482 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 482 insertions(+), 476 deletions(-) create mode 100644 vibes/execution_calls.go diff --git a/vibes/execution.go b/vibes/execution.go index b4d7e44..ee2408c 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -625,479 +625,3 @@ func (exec *Execution) evalExpressionWithAuto(expr Expression, env *Env, autoCal return NewNil(), exec.errorAt(expr.Pos(), "unsupported expression") } } - -func (exec *Execution) autoInvokeIfNeeded(expr Expression, val Value, receiver Value) (Value, error) { - switch val.Kind() { - case KindFunction: - fn := val.Function() - if fn != nil && len(fn.Params) == 0 { - return exec.invokeCallable(val, receiver, nil, nil, NewNil(), expr.Pos()) - } - case KindBuiltin: - builtin := val.Builtin() - if builtin != nil && builtin.AutoInvoke { - return exec.invokeCallable(val, receiver, nil, nil, NewNil(), expr.Pos()) - } - } - return val, nil -} - -func (exec *Execution) invokeCallable(callee Value, receiver Value, args []Value, kwargs map[string]Value, block Value, pos Position) (Value, error) { - switch callee.Kind() { - case KindFunction: - result, err := exec.callFunction(callee.Function(), receiver, args, kwargs, block, pos) - if err != nil { - if errors.Is(err, errLoopBreak) { - return NewNil(), exec.errorAt(pos, "break cannot cross call boundary") - } - if errors.Is(err, errLoopNext) { - return NewNil(), exec.errorAt(pos, "next cannot cross call boundary") - } - return NewNil(), err - } - return result, nil - case KindBuiltin: - builtin := callee.Builtin() - scope := exec.capabilityContractScopes[builtin] - var preCallKnownBuiltins map[*Builtin]struct{} - if scope != nil && len(scope.contracts) > 0 { - preCallKnownBuiltins = make(map[*Builtin]struct{}) - if receiver.Kind() != KindNil { - collectCapabilityBuiltins(receiver, preCallKnownBuiltins) - } - for _, root := range scope.roots { - collectCapabilityBuiltins(root, preCallKnownBuiltins) - } - for _, arg := range args { - collectCapabilityBuiltins(arg, preCallKnownBuiltins) - } - for _, kwarg := range kwargs { - collectCapabilityBuiltins(kwarg, preCallKnownBuiltins) - } - } - contract, hasContract := exec.capabilityContracts[builtin] - if hasContract && contract.ValidateArgs != nil { - if err := contract.ValidateArgs(args, kwargs, block); err != nil { - return NewNil(), exec.wrapError(err, pos) - } - } - - result, err := builtin.Fn(exec, receiver, args, kwargs, block) - if err != nil { - if errors.Is(err, errLoopBreak) { - return NewNil(), exec.errorAt(pos, "break cannot cross call boundary") - } - if errors.Is(err, errLoopNext) { - return NewNil(), exec.errorAt(pos, "next cannot cross call boundary") - } - return NewNil(), exec.wrapError(err, pos) - } - if hasContract && contract.ValidateReturn != nil { - if err := contract.ValidateReturn(result); err != nil { - return NewNil(), exec.wrapError(err, pos) - } - } - if scope != nil && len(scope.contracts) > 0 { - // Capability methods can lazily publish additional builtins at runtime - // (e.g. through factory return values or receiver mutation). Re-scan - // these values so future calls still enforce declared contracts. - bindCapabilityContractsExcluding(result, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) - if receiver.Kind() != KindNil { - bindCapabilityContractsExcluding(receiver, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) - } - // Methods can mutate sibling scope roots via captured references; refresh - // all adapter roots so newly exposed builtins also get bound. - for _, root := range scope.roots { - bindCapabilityContractsExcluding(root, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) - } - // Methods can also publish builtins by mutating positional or keyword - // argument objects supplied by script code. - for _, arg := range args { - bindCapabilityContractsExcluding(arg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) - } - for _, kwarg := range kwargs { - bindCapabilityContractsExcluding(kwarg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) - } - } - return result, nil - default: - return NewNil(), exec.errorAt(pos, "attempted to call non-callable value") - } -} - -func (exec *Execution) callFunction(fn *ScriptFunction, receiver Value, args []Value, kwargs map[string]Value, block Value, pos Position) (Value, error) { - callEnv := newEnv(fn.Env) - if receiver.Kind() != KindNil { - callEnv.Define("self", receiver) - } - callEnv.Define("__block__", block) - if err := exec.bindFunctionArgs(fn, callEnv, args, kwargs, pos); err != nil { - return NewNil(), err - } - exec.pushEnv(callEnv) - if err := exec.checkMemory(); err != nil { - exec.popEnv() - return NewNil(), err - } - exec.popEnv() - if err := exec.pushFrame(fn.Name, pos); err != nil { - return NewNil(), err - } - - ctx := moduleContext{} - if fn.owner != nil { - ctx = moduleContext{ - key: fn.owner.moduleKey, - path: fn.owner.modulePath, - root: fn.owner.moduleRoot, - } - } - exec.pushModuleContext(ctx) - exec.pushReceiver(receiver) - val, returned, err := exec.evalStatements(fn.Body, callEnv) - exec.popReceiver() - exec.popModuleContext() - exec.popFrame() - if err != nil { - return NewNil(), err - } - if fn.ReturnTy != nil { - if err := checkValueType(val, fn.ReturnTy); err != nil { - return NewNil(), exec.errorAt(pos, "%s", formatReturnTypeMismatch(fn.Name, err)) - } - } - if returned { - return val, nil - } - return val, nil -} - -func (exec *Execution) evalUnaryExpr(e *UnaryExpr, env *Env) (Value, error) { - right, err := exec.evalExpressionWithAuto(e.Right, env, true) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(right); err != nil { - return NewNil(), err - } - switch e.Operator { - case tokenMinus: - switch right.Kind() { - case KindInt: - return NewInt(-right.Int()), nil - case KindFloat: - return NewFloat(-right.Float()), nil - default: - return NewNil(), exec.errorAt(e.Pos(), "unsupported unary - operand") - } - case tokenBang: - return NewBool(!right.Truthy()), nil - default: - return NewNil(), exec.errorAt(e.Pos(), "unsupported unary operator") - } -} - -func (exec *Execution) evalIndexExpr(e *IndexExpr, env *Env) (Value, error) { - obj, err := exec.evalExpressionWithAuto(e.Object, env, true) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(obj); err != nil { - return NewNil(), err - } - idx, err := exec.evalExpressionWithAuto(e.Index, env, true) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(idx); err != nil { - return NewNil(), err - } - switch obj.Kind() { - case KindString: - i, err := valueToInt(idx) - if err != nil { - return NewNil(), exec.errorAt(e.Index.Pos(), "%s", err.Error()) - } - runes := []rune(obj.String()) - if i < 0 || i >= len(runes) { - return NewNil(), exec.errorAt(e.Index.Pos(), "string index out of bounds") - } - return NewString(string(runes[i])), nil - case KindArray: - i, err := valueToInt(idx) - if err != nil { - return NewNil(), exec.errorAt(e.Index.Pos(), "%s", err.Error()) - } - arr := obj.Array() - if i < 0 || i >= len(arr) { - return NewNil(), exec.errorAt(e.Index.Pos(), "array index out of bounds") - } - return arr[i], nil - case KindHash, KindObject: - key, err := valueToHashKey(idx) - if err != nil { - return NewNil(), exec.errorAt(e.Index.Pos(), "%s", err.Error()) - } - val, ok := obj.Hash()[key] - if !ok { - return NewNil(), nil - } - return val, nil - default: - return NewNil(), exec.errorAt(e.Object.Pos(), "cannot index %s", obj.Kind()) - } -} - -func (exec *Execution) evalBinaryExpr(expr *BinaryExpr, env *Env) (Value, error) { - left, err := exec.evalExpression(expr.Left, env) - if err != nil { - return NewNil(), err - } - right, err := exec.evalExpression(expr.Right, env) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(left, right); err != nil { - return NewNil(), err - } - - var result Value - switch expr.Operator { - case tokenPlus: - result, err = addValues(left, right) - case tokenMinus: - result, err = subtractValues(left, right) - case tokenAsterisk: - result, err = multiplyValues(left, right) - case tokenSlash: - result, err = divideValues(left, right) - case tokenPercent: - result, err = moduloValues(left, right) - case tokenEQ: - return NewBool(left.Equal(right)), nil - case tokenNotEQ: - return NewBool(!left.Equal(right)), nil - case tokenLT: - return compareValues(expr, left, right, func(c int) bool { return c < 0 }) - case tokenLTE: - return compareValues(expr, left, right, func(c int) bool { return c <= 0 }) - case tokenGT: - return compareValues(expr, left, right, func(c int) bool { return c > 0 }) - case tokenGTE: - return compareValues(expr, left, right, func(c int) bool { return c >= 0 }) - case tokenAnd: - return NewBool(left.Truthy() && right.Truthy()), nil - case tokenOr: - return NewBool(left.Truthy() || right.Truthy()), nil - default: - return NewNil(), exec.errorAt(expr.Pos(), "unsupported operator") - } - - if err != nil { - return NewNil(), exec.wrapError(err, expr.Pos()) - } - return result, nil -} - -func (exec *Execution) evalCallTarget(call *CallExpr, env *Env) (Value, Value, error) { - if member, ok := call.Callee.(*MemberExpr); ok { - receiver, err := exec.evalExpression(member.Object, env) - if err != nil { - return NewNil(), NewNil(), err - } - if err := exec.checkMemoryWith(receiver); err != nil { - return NewNil(), NewNil(), err - } - callee, err := exec.getMember(receiver, member.Property, member.Pos()) - if err != nil { - return NewNil(), NewNil(), err - } - return callee, receiver, nil - } - - callee, err := exec.evalExpressionWithAuto(call.Callee, env, false) - if err != nil { - return NewNil(), NewNil(), err - } - return callee, NewNil(), nil -} - -func (exec *Execution) evalCallArgs(call *CallExpr, env *Env) ([]Value, error) { - args := make([]Value, len(call.Args)) - for i, arg := range call.Args { - val, err := exec.evalExpressionWithAuto(arg, env, true) - if err != nil { - return nil, err - } - if err := exec.checkMemoryWith(val); err != nil { - return nil, err - } - args[i] = val - } - return args, nil -} - -func (exec *Execution) evalCallKwArgs(call *CallExpr, env *Env) (map[string]Value, error) { - if len(call.KwArgs) == 0 { - return nil, nil - } - kwargs := make(map[string]Value, len(call.KwArgs)) - for _, kw := range call.KwArgs { - val, err := exec.evalExpressionWithAuto(kw.Value, env, true) - if err != nil { - return nil, err - } - if err := exec.checkMemoryWith(val); err != nil { - return nil, err - } - kwargs[kw.Name] = val - } - return kwargs, nil -} - -func (exec *Execution) evalCallBlock(call *CallExpr, env *Env) (Value, error) { - if call.Block == nil { - return NewNil(), nil - } - return exec.evalBlockLiteral(call.Block, env) -} - -func (exec *Execution) checkCallMemoryRoots(receiver Value, args []Value, kwargs map[string]Value, block Value) error { - if receiver.Kind() == KindNil && len(kwargs) == 0 && block.IsNil() { - if len(args) == 0 { - return nil - } - return exec.checkMemoryWith(args...) - } - combined := make([]Value, 0, len(args)+len(kwargs)+2) - if receiver.Kind() != KindNil { - combined = append(combined, receiver) - } - combined = append(combined, args...) - for _, kwVal := range kwargs { - combined = append(combined, kwVal) - } - if !block.IsNil() { - combined = append(combined, block) - } - if len(combined) == 0 { - return nil - } - return exec.checkMemoryWith(combined...) -} - -func (exec *Execution) evalCallExpr(call *CallExpr, env *Env) (Value, error) { - callee, receiver, err := exec.evalCallTarget(call, env) - if err != nil { - return NewNil(), err - } - args, err := exec.evalCallArgs(call, env) - if err != nil { - return NewNil(), err - } - kwargs, err := exec.evalCallKwArgs(call, env) - if err != nil { - return NewNil(), err - } - block, err := exec.evalCallBlock(call, env) - if err != nil { - return NewNil(), err - } - if err := exec.checkCallMemoryRoots(receiver, args, kwargs, block); err != nil { - return NewNil(), err - } - - result, callErr := exec.invokeCallable(callee, receiver, args, kwargs, block, call.Pos()) - if callErr != nil { - return NewNil(), callErr - } - if err := exec.checkMemoryWith(result); err != nil { - return NewNil(), err - } - return result, nil -} - -func (exec *Execution) evalBlockLiteral(block *BlockLiteral, env *Env) (Value, error) { - blockValue := NewBlock(block.Params, block.Body, env) - if ctx := exec.currentModuleContext(); ctx != nil { - blk := blockValue.Block() - blk.moduleKey = ctx.key - blk.modulePath = ctx.path - blk.moduleRoot = ctx.root - } - return blockValue, nil -} - -func ensureBlock(block Value, name string) error { - if block.Block() == nil { - if name != "" { - return fmt.Errorf("%s requires a block", name) - } - return fmt.Errorf("block required") - } - return nil -} - -// CallBlock invokes a block value with the provided arguments. -// This is the public entry point for capability adapters that need to -// call user-supplied blocks (e.g. db.each, db.tx). -func (exec *Execution) CallBlock(block Value, args []Value) (Value, error) { - if err := ensureBlock(block, ""); err != nil { - return NewNil(), err - } - blk := block.Block() - exec.pushModuleContext(moduleContext{ - key: blk.moduleKey, - path: blk.modulePath, - root: blk.moduleRoot, - }) - defer exec.popModuleContext() - - blockEnv := newEnv(blk.Env) - for i, param := range blk.Params { - var val Value - if i < len(args) { - val = args[i] - } else { - val = NewNil() - } - if param.Type != nil { - if err := checkValueType(val, param.Type); err != nil { - return NewNil(), exec.errorAt(param.Type.position, "%s", formatArgumentTypeMismatch(param.Name, err)) - } - } - blockEnv.Define(param.Name, val) - } - val, returned, err := exec.evalStatements(blk.Body, blockEnv) - if err != nil { - return NewNil(), err - } - if returned { - return val, nil - } - return val, nil -} - -func (exec *Execution) evalYield(expr *YieldExpr, env *Env) (Value, error) { - block, ok := env.Get("__block__") - if !ok || block.Kind() == KindNil { - return NewNil(), exec.errorAt(expr.Pos(), "no block given") - } - var args []Value - for _, arg := range expr.Args { - val, err := exec.evalExpression(arg, env) - if err != nil { - return NewNil(), err - } - if err := exec.checkMemoryWith(val); err != nil { - return NewNil(), err - } - args = append(args, val) - } - if len(args) > 0 { - if err := exec.checkMemoryWith(args...); err != nil { - return NewNil(), err - } - } - return exec.CallBlock(block, args) -} diff --git a/vibes/execution_calls.go b/vibes/execution_calls.go new file mode 100644 index 0000000..fb15fb4 --- /dev/null +++ b/vibes/execution_calls.go @@ -0,0 +1,482 @@ +package vibes + +import ( + "errors" + "fmt" +) + +func (exec *Execution) autoInvokeIfNeeded(expr Expression, val Value, receiver Value) (Value, error) { + switch val.Kind() { + case KindFunction: + fn := val.Function() + if fn != nil && len(fn.Params) == 0 { + return exec.invokeCallable(val, receiver, nil, nil, NewNil(), expr.Pos()) + } + case KindBuiltin: + builtin := val.Builtin() + if builtin != nil && builtin.AutoInvoke { + return exec.invokeCallable(val, receiver, nil, nil, NewNil(), expr.Pos()) + } + } + return val, nil +} + +func (exec *Execution) invokeCallable(callee Value, receiver Value, args []Value, kwargs map[string]Value, block Value, pos Position) (Value, error) { + switch callee.Kind() { + case KindFunction: + result, err := exec.callFunction(callee.Function(), receiver, args, kwargs, block, pos) + if err != nil { + if errors.Is(err, errLoopBreak) { + return NewNil(), exec.errorAt(pos, "break cannot cross call boundary") + } + if errors.Is(err, errLoopNext) { + return NewNil(), exec.errorAt(pos, "next cannot cross call boundary") + } + return NewNil(), err + } + return result, nil + case KindBuiltin: + builtin := callee.Builtin() + scope := exec.capabilityContractScopes[builtin] + var preCallKnownBuiltins map[*Builtin]struct{} + if scope != nil && len(scope.contracts) > 0 { + preCallKnownBuiltins = make(map[*Builtin]struct{}) + if receiver.Kind() != KindNil { + collectCapabilityBuiltins(receiver, preCallKnownBuiltins) + } + for _, root := range scope.roots { + collectCapabilityBuiltins(root, preCallKnownBuiltins) + } + for _, arg := range args { + collectCapabilityBuiltins(arg, preCallKnownBuiltins) + } + for _, kwarg := range kwargs { + collectCapabilityBuiltins(kwarg, preCallKnownBuiltins) + } + } + contract, hasContract := exec.capabilityContracts[builtin] + if hasContract && contract.ValidateArgs != nil { + if err := contract.ValidateArgs(args, kwargs, block); err != nil { + return NewNil(), exec.wrapError(err, pos) + } + } + + result, err := builtin.Fn(exec, receiver, args, kwargs, block) + if err != nil { + if errors.Is(err, errLoopBreak) { + return NewNil(), exec.errorAt(pos, "break cannot cross call boundary") + } + if errors.Is(err, errLoopNext) { + return NewNil(), exec.errorAt(pos, "next cannot cross call boundary") + } + return NewNil(), exec.wrapError(err, pos) + } + if hasContract && contract.ValidateReturn != nil { + if err := contract.ValidateReturn(result); err != nil { + return NewNil(), exec.wrapError(err, pos) + } + } + if scope != nil && len(scope.contracts) > 0 { + // Capability methods can lazily publish additional builtins at runtime + // (e.g. through factory return values or receiver mutation). Re-scan + // these values so future calls still enforce declared contracts. + bindCapabilityContractsExcluding(result, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) + if receiver.Kind() != KindNil { + bindCapabilityContractsExcluding(receiver, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) + } + // Methods can mutate sibling scope roots via captured references; refresh + // all adapter roots so newly exposed builtins also get bound. + for _, root := range scope.roots { + bindCapabilityContractsExcluding(root, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) + } + // Methods can also publish builtins by mutating positional or keyword + // argument objects supplied by script code. + for _, arg := range args { + bindCapabilityContractsExcluding(arg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) + } + for _, kwarg := range kwargs { + bindCapabilityContractsExcluding(kwarg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins) + } + } + return result, nil + default: + return NewNil(), exec.errorAt(pos, "attempted to call non-callable value") + } +} + +func (exec *Execution) callFunction(fn *ScriptFunction, receiver Value, args []Value, kwargs map[string]Value, block Value, pos Position) (Value, error) { + callEnv := newEnv(fn.Env) + if receiver.Kind() != KindNil { + callEnv.Define("self", receiver) + } + callEnv.Define("__block__", block) + if err := exec.bindFunctionArgs(fn, callEnv, args, kwargs, pos); err != nil { + return NewNil(), err + } + exec.pushEnv(callEnv) + if err := exec.checkMemory(); err != nil { + exec.popEnv() + return NewNil(), err + } + exec.popEnv() + if err := exec.pushFrame(fn.Name, pos); err != nil { + return NewNil(), err + } + + ctx := moduleContext{} + if fn.owner != nil { + ctx = moduleContext{ + key: fn.owner.moduleKey, + path: fn.owner.modulePath, + root: fn.owner.moduleRoot, + } + } + exec.pushModuleContext(ctx) + exec.pushReceiver(receiver) + val, returned, err := exec.evalStatements(fn.Body, callEnv) + exec.popReceiver() + exec.popModuleContext() + exec.popFrame() + if err != nil { + return NewNil(), err + } + if fn.ReturnTy != nil { + if err := checkValueType(val, fn.ReturnTy); err != nil { + return NewNil(), exec.errorAt(pos, "%s", formatReturnTypeMismatch(fn.Name, err)) + } + } + if returned { + return val, nil + } + return val, nil +} + +func (exec *Execution) evalUnaryExpr(e *UnaryExpr, env *Env) (Value, error) { + right, err := exec.evalExpressionWithAuto(e.Right, env, true) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(right); err != nil { + return NewNil(), err + } + switch e.Operator { + case tokenMinus: + switch right.Kind() { + case KindInt: + return NewInt(-right.Int()), nil + case KindFloat: + return NewFloat(-right.Float()), nil + default: + return NewNil(), exec.errorAt(e.Pos(), "unsupported unary - operand") + } + case tokenBang: + return NewBool(!right.Truthy()), nil + default: + return NewNil(), exec.errorAt(e.Pos(), "unsupported unary operator") + } +} + +func (exec *Execution) evalIndexExpr(e *IndexExpr, env *Env) (Value, error) { + obj, err := exec.evalExpressionWithAuto(e.Object, env, true) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(obj); err != nil { + return NewNil(), err + } + idx, err := exec.evalExpressionWithAuto(e.Index, env, true) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(idx); err != nil { + return NewNil(), err + } + switch obj.Kind() { + case KindString: + i, err := valueToInt(idx) + if err != nil { + return NewNil(), exec.errorAt(e.Index.Pos(), "%s", err.Error()) + } + runes := []rune(obj.String()) + if i < 0 || i >= len(runes) { + return NewNil(), exec.errorAt(e.Index.Pos(), "string index out of bounds") + } + return NewString(string(runes[i])), nil + case KindArray: + i, err := valueToInt(idx) + if err != nil { + return NewNil(), exec.errorAt(e.Index.Pos(), "%s", err.Error()) + } + arr := obj.Array() + if i < 0 || i >= len(arr) { + return NewNil(), exec.errorAt(e.Index.Pos(), "array index out of bounds") + } + return arr[i], nil + case KindHash, KindObject: + key, err := valueToHashKey(idx) + if err != nil { + return NewNil(), exec.errorAt(e.Index.Pos(), "%s", err.Error()) + } + val, ok := obj.Hash()[key] + if !ok { + return NewNil(), nil + } + return val, nil + default: + return NewNil(), exec.errorAt(e.Object.Pos(), "cannot index %s", obj.Kind()) + } +} + +func (exec *Execution) evalBinaryExpr(expr *BinaryExpr, env *Env) (Value, error) { + left, err := exec.evalExpression(expr.Left, env) + if err != nil { + return NewNil(), err + } + right, err := exec.evalExpression(expr.Right, env) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(left, right); err != nil { + return NewNil(), err + } + + var result Value + switch expr.Operator { + case tokenPlus: + result, err = addValues(left, right) + case tokenMinus: + result, err = subtractValues(left, right) + case tokenAsterisk: + result, err = multiplyValues(left, right) + case tokenSlash: + result, err = divideValues(left, right) + case tokenPercent: + result, err = moduloValues(left, right) + case tokenEQ: + return NewBool(left.Equal(right)), nil + case tokenNotEQ: + return NewBool(!left.Equal(right)), nil + case tokenLT: + return compareValues(expr, left, right, func(c int) bool { return c < 0 }) + case tokenLTE: + return compareValues(expr, left, right, func(c int) bool { return c <= 0 }) + case tokenGT: + return compareValues(expr, left, right, func(c int) bool { return c > 0 }) + case tokenGTE: + return compareValues(expr, left, right, func(c int) bool { return c >= 0 }) + case tokenAnd: + return NewBool(left.Truthy() && right.Truthy()), nil + case tokenOr: + return NewBool(left.Truthy() || right.Truthy()), nil + default: + return NewNil(), exec.errorAt(expr.Pos(), "unsupported operator") + } + + if err != nil { + return NewNil(), exec.wrapError(err, expr.Pos()) + } + return result, nil +} + +func (exec *Execution) evalCallTarget(call *CallExpr, env *Env) (Value, Value, error) { + if member, ok := call.Callee.(*MemberExpr); ok { + receiver, err := exec.evalExpression(member.Object, env) + if err != nil { + return NewNil(), NewNil(), err + } + if err := exec.checkMemoryWith(receiver); err != nil { + return NewNil(), NewNil(), err + } + callee, err := exec.getMember(receiver, member.Property, member.Pos()) + if err != nil { + return NewNil(), NewNil(), err + } + return callee, receiver, nil + } + + callee, err := exec.evalExpressionWithAuto(call.Callee, env, false) + if err != nil { + return NewNil(), NewNil(), err + } + return callee, NewNil(), nil +} + +func (exec *Execution) evalCallArgs(call *CallExpr, env *Env) ([]Value, error) { + args := make([]Value, len(call.Args)) + for i, arg := range call.Args { + val, err := exec.evalExpressionWithAuto(arg, env, true) + if err != nil { + return nil, err + } + if err := exec.checkMemoryWith(val); err != nil { + return nil, err + } + args[i] = val + } + return args, nil +} + +func (exec *Execution) evalCallKwArgs(call *CallExpr, env *Env) (map[string]Value, error) { + if len(call.KwArgs) == 0 { + return nil, nil + } + kwargs := make(map[string]Value, len(call.KwArgs)) + for _, kw := range call.KwArgs { + val, err := exec.evalExpressionWithAuto(kw.Value, env, true) + if err != nil { + return nil, err + } + if err := exec.checkMemoryWith(val); err != nil { + return nil, err + } + kwargs[kw.Name] = val + } + return kwargs, nil +} + +func (exec *Execution) evalCallBlock(call *CallExpr, env *Env) (Value, error) { + if call.Block == nil { + return NewNil(), nil + } + return exec.evalBlockLiteral(call.Block, env) +} + +func (exec *Execution) checkCallMemoryRoots(receiver Value, args []Value, kwargs map[string]Value, block Value) error { + if receiver.Kind() == KindNil && len(kwargs) == 0 && block.IsNil() { + if len(args) == 0 { + return nil + } + return exec.checkMemoryWith(args...) + } + combined := make([]Value, 0, len(args)+len(kwargs)+2) + if receiver.Kind() != KindNil { + combined = append(combined, receiver) + } + combined = append(combined, args...) + for _, kwVal := range kwargs { + combined = append(combined, kwVal) + } + if !block.IsNil() { + combined = append(combined, block) + } + if len(combined) == 0 { + return nil + } + return exec.checkMemoryWith(combined...) +} + +func (exec *Execution) evalCallExpr(call *CallExpr, env *Env) (Value, error) { + callee, receiver, err := exec.evalCallTarget(call, env) + if err != nil { + return NewNil(), err + } + args, err := exec.evalCallArgs(call, env) + if err != nil { + return NewNil(), err + } + kwargs, err := exec.evalCallKwArgs(call, env) + if err != nil { + return NewNil(), err + } + block, err := exec.evalCallBlock(call, env) + if err != nil { + return NewNil(), err + } + if err := exec.checkCallMemoryRoots(receiver, args, kwargs, block); err != nil { + return NewNil(), err + } + + result, callErr := exec.invokeCallable(callee, receiver, args, kwargs, block, call.Pos()) + if callErr != nil { + return NewNil(), callErr + } + if err := exec.checkMemoryWith(result); err != nil { + return NewNil(), err + } + return result, nil +} + +func (exec *Execution) evalBlockLiteral(block *BlockLiteral, env *Env) (Value, error) { + blockValue := NewBlock(block.Params, block.Body, env) + if ctx := exec.currentModuleContext(); ctx != nil { + blk := blockValue.Block() + blk.moduleKey = ctx.key + blk.modulePath = ctx.path + blk.moduleRoot = ctx.root + } + return blockValue, nil +} + +func ensureBlock(block Value, name string) error { + if block.Block() == nil { + if name != "" { + return fmt.Errorf("%s requires a block", name) + } + return fmt.Errorf("block required") + } + return nil +} + +// CallBlock invokes a block value with the provided arguments. +// This is the public entry point for capability adapters that need to +// call user-supplied blocks (e.g. db.each, db.tx). +func (exec *Execution) CallBlock(block Value, args []Value) (Value, error) { + if err := ensureBlock(block, ""); err != nil { + return NewNil(), err + } + blk := block.Block() + exec.pushModuleContext(moduleContext{ + key: blk.moduleKey, + path: blk.modulePath, + root: blk.moduleRoot, + }) + defer exec.popModuleContext() + + blockEnv := newEnv(blk.Env) + for i, param := range blk.Params { + var val Value + if i < len(args) { + val = args[i] + } else { + val = NewNil() + } + if param.Type != nil { + if err := checkValueType(val, param.Type); err != nil { + return NewNil(), exec.errorAt(param.Type.position, "%s", formatArgumentTypeMismatch(param.Name, err)) + } + } + blockEnv.Define(param.Name, val) + } + val, returned, err := exec.evalStatements(blk.Body, blockEnv) + if err != nil { + return NewNil(), err + } + if returned { + return val, nil + } + return val, nil +} + +func (exec *Execution) evalYield(expr *YieldExpr, env *Env) (Value, error) { + block, ok := env.Get("__block__") + if !ok || block.Kind() == KindNil { + return NewNil(), exec.errorAt(expr.Pos(), "no block given") + } + var args []Value + for _, arg := range expr.Args { + val, err := exec.evalExpression(arg, env) + if err != nil { + return NewNil(), err + } + if err := exec.checkMemoryWith(val); err != nil { + return NewNil(), err + } + args = append(args, val) + } + if len(args) > 0 { + if err := exec.checkMemoryWith(args...); err != nil { + return NewNil(), err + } + } + return exec.CallBlock(block, args) +} From 1a97b19e7fbcf1167492acaf8d15bff32a81872a Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Fri, 20 Feb 2026 22:17:06 -0500 Subject: [PATCH 15/99] isolate runtime error model and wrapping helpers --- vibes/execution.go | 182 ------------------------------------- vibes/execution_errors.go | 187 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 182 deletions(-) create mode 100644 vibes/execution_errors.go diff --git a/vibes/execution.go b/vibes/execution.go index ee2408c..c8fa4f3 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -3,9 +3,6 @@ package vibes import ( "context" "errors" - "fmt" - "regexp" - "strings" ) type ScriptFunction struct { @@ -78,185 +75,6 @@ type callFrame struct { Pos Position } -type StackFrame struct { - Function string - Pos Position -} - -type RuntimeError struct { - Type string - Message string - CodeFrame string - Frames []StackFrame -} - -type assertionFailureError struct { - message string -} - -func (e *assertionFailureError) Error() string { - return e.message -} - -const ( - runtimeErrorTypeBase = "RuntimeError" - runtimeErrorTypeAssertion = "AssertionError" - runtimeErrorFrameHead = 8 - runtimeErrorFrameTail = 8 -) - -var ( - errLoopBreak = errors.New("loop break") - errLoopNext = errors.New("loop next") - errStepQuotaExceeded = errors.New("step quota exceeded") - errMemoryQuotaExceeded = errors.New("memory quota exceeded") - stringTemplatePattern = regexp.MustCompile(`\{\{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*\}\}`) -) - -func (re *RuntimeError) Error() string { - var b strings.Builder - b.WriteString(re.Message) - if re.CodeFrame != "" { - b.WriteString("\n") - b.WriteString(re.CodeFrame) - } - renderFrame := func(frame StackFrame) { - if frame.Pos.Line > 0 && frame.Pos.Column > 0 { - fmt.Fprintf(&b, "\n at %s (%d:%d)", frame.Function, frame.Pos.Line, frame.Pos.Column) - } else if frame.Pos.Line > 0 { - fmt.Fprintf(&b, "\n at %s (line %d)", frame.Function, frame.Pos.Line) - } else { - fmt.Fprintf(&b, "\n at %s", frame.Function) - } - } - - if len(re.Frames) <= runtimeErrorFrameHead+runtimeErrorFrameTail { - for _, frame := range re.Frames { - renderFrame(frame) - } - return b.String() - } - - for _, frame := range re.Frames[:runtimeErrorFrameHead] { - renderFrame(frame) - } - omitted := len(re.Frames) - (runtimeErrorFrameHead + runtimeErrorFrameTail) - fmt.Fprintf(&b, "\n ... %d frames omitted ...", omitted) - for _, frame := range re.Frames[len(re.Frames)-runtimeErrorFrameTail:] { - renderFrame(frame) - } - - return b.String() -} - -// Unwrap returns nil to satisfy the error unwrapping interface. -// RuntimeError is a terminal error that wraps the original error message but not the error itself. -func (re *RuntimeError) Unwrap() error { - return nil -} - -func canonicalRuntimeErrorType(name string) (string, bool) { - switch { - case strings.EqualFold(name, runtimeErrorTypeBase), strings.EqualFold(name, "Error"): - return runtimeErrorTypeBase, true - case strings.EqualFold(name, runtimeErrorTypeAssertion): - return runtimeErrorTypeAssertion, true - default: - return "", false - } -} - -func classifyRuntimeErrorType(err error) string { - if err == nil { - return runtimeErrorTypeBase - } - var assertionErr *assertionFailureError - if errors.As(err, &assertionErr) { - return runtimeErrorTypeAssertion - } - if runtimeErr, ok := err.(*RuntimeError); ok { - if kind, known := canonicalRuntimeErrorType(runtimeErr.Type); known { - return kind - } - } - return runtimeErrorTypeBase -} - -func newAssertionFailureError(message string) error { - return &assertionFailureError{message: message} -} - -func (exec *Execution) step() error { - exec.steps++ - if exec.quota > 0 && exec.steps > exec.quota { - return fmt.Errorf("%w (%d)", errStepQuotaExceeded, exec.quota) - } - if exec.memoryQuota > 0 && (exec.steps&15) == 0 { - if err := exec.checkMemory(); err != nil { - return err - } - } - if exec.ctx != nil { - select { - case <-exec.ctx.Done(): - return exec.ctx.Err() - default: - } - } - return nil -} - -func (exec *Execution) errorAt(pos Position, format string, args ...any) error { - return exec.newRuntimeError(fmt.Sprintf(format, args...), pos) -} - -func (exec *Execution) newRuntimeError(message string, pos Position) error { - return exec.newRuntimeErrorWithType(runtimeErrorTypeBase, message, pos) -} - -func (exec *Execution) newRuntimeErrorWithType(kind string, message string, pos Position) error { - if canonical, ok := canonicalRuntimeErrorType(kind); ok { - kind = canonical - } else { - kind = runtimeErrorTypeBase - } - - frames := make([]StackFrame, 0, len(exec.callStack)+1) - - if len(exec.callStack) > 0 { - // First frame: where the error occurred (within the current function) - current := exec.callStack[len(exec.callStack)-1] - frames = append(frames, StackFrame{Function: current.Function, Pos: pos}) - - // Remaining frames: the call stack (where each function was called from) - for i := len(exec.callStack) - 1; i >= 0; i-- { - cf := exec.callStack[i] - frames = append(frames, StackFrame(cf)) - } - } else { - // No call stack means error at script top level - frames = append(frames, StackFrame{Function: "