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..6712851 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,171 @@ +# 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` (execution engine types/state container) +- `vibes/execution_statements.go` (statement dispatch and execution) +- `vibes/execution_expressions.go` (expression dispatch and evaluation) +- `vibes/execution_assign.go` (assignment targets and member assignment flow) +- `vibes/execution_script.go` (script call surface and call-time orchestration) +- `vibes/execution_script_helpers.go` (compiled script lookup/order/ownership and call-time cloning helpers) +- `vibes/execution_call_execution.go` (execution struct bootstrap for script calls) +- `vibes/execution_call_capabilities.go` (call-time capability binding and contract registration) +- `vibes/execution_call_globals.go` (strict global validation and call-time global binding) +- `vibes/execution_call_classes.go` (call-time class body initialization in class-local scope) +- `vibes/execution_call_env.go` (call env prep: argument rebind/bind and memory validation) +- `vibes/execution_call_invoke.go` (function frame execution, return type validation, memory checks) +- `vibes/execution_function_args.go` (function argument/default/type/ivar binding helpers) +- `vibes/execution_calls.go` (callable dispatch + function invocation) +- `vibes/execution_call_expr.go` (call expression target/args/kwargs/block evaluation) +- `vibes/execution_blocks.go` (block literal creation and block/yield invocation) +- `vibes/execution_operators.go` (unary/index/binary operator evaluation) +- `vibes/execution_control.go` (range/case/loop/try evaluation) +- `vibes/execution_loops.go` (`for`/`while`/`until` loop execution) +- `vibes/execution_try_raise.go` (raise/try-rescue-ensure execution flow) +- `vibes/execution_rescue_types.go` (rescue type matching and control-signal classification helpers) +- `vibes/execution_errors.go` (runtime error model, wrapping, and quota/signal sentinels) +- `vibes/execution_state.go` (runtime call/env/module/receiver stack helpers) +- `vibes/execution_members.go` (member dispatch for runtime values) +- `vibes/execution_members_class_instance.go` (class/instance member access and callable wrapper binding) +- `vibes/execution_members_numeric.go` (int/float/money member behavior) +- `vibes/execution_members_hash.go` (hash/object member dispatch) +- `vibes/execution_members_hash_query.go` (hash query and enumeration member methods) +- `vibes/execution_members_hash_transforms.go` (hash filter/transform/member mutation methods) +- `vibes/execution_members_hash_deep.go` (`hash.deep_transform_keys` recursion/cycle handling) +- `vibes/execution_members_string.go` (string member dispatch) +- `vibes/execution_members_string_query.go` (string query/search member methods) +- `vibes/execution_members_string_transforms.go` (string transform/normalization member methods) +- `vibes/execution_members_string_textops.go` (string substitution/splitting/template member methods) +- `vibes/execution_members_string_helpers.go` (string helper routines for member methods) +- `vibes/execution_members_duration.go` (duration member behavior) +- `vibes/execution_members_array.go` (array member dispatch) +- `vibes/execution_members_array_query.go` (array query/enumeration member methods) +- `vibes/execution_members_array_transforms.go` (array mutation/transform member methods) +- `vibes/execution_members_array_grouping.go` (array sort/group/tally member methods) +- `vibes/execution_types.go` (type mismatch and declared-type formatting helpers) +- `vibes/execution_types_validation.go` (runtime value-vs-type validation with recursion guards) +- `vibes/execution_types_value_format.go` (runtime value-type formatting helpers) +- `vibes/execution_values.go` (value conversion, sorting, and flattening helpers) +- `vibes/execution_values_arithmetic.go` (value arithmetic and comparison operators) + +## 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 initialization + token stream helpers) +- `vibes/parser_errors.go` (parse errors and token labeling) +- `vibes/parser_expressions.go` (expression dispatch loop) +- `vibes/parser_operator_expressions.go` (grouped/prefix/infix/range expression parsing) +- `vibes/parser_access_expressions.go` (member and index expression parsing) +- `vibes/parser_yield_literals.go` (`yield` expression argument parsing) +- `vibes/parser_case_literals.go` (case/when/else expression parsing) +- `vibes/parser_call_literals.go` (call argument parsing, keyword labels, and call blocks) +- `vibes/parser_literals.go` (identifier and scalar literal parsing) +- `vibes/parser_collection_literals.go` (array/hash literal parsing and symbol-style hash pairs) +- `vibes/parser_block_literals.go` (block literals, block params, and typed union param parsing) +- `vibes/parser_statements.go` (statement dispatch + return/raise/block parsing) +- `vibes/parser_expression_statements.go` (expression/assert/assignment statement parsing) +- `vibes/parser_declarations.go` (function declaration parsing) +- `vibes/parser_class_declarations.go` (class declaration parsing) +- `vibes/parser_function_modifiers.go` (top-level `export`/`private` function declaration parsing) +- `vibes/parser_declaration_helpers.go` (parameter list and property declaration parsing) +- `vibes/parser_control.go` (if/loop/begin-rescue-ensure parsing) +- `vibes/parser_precedence.go` (precedence table + assignable-expression helpers) +- `vibes/parser_types.go` (type-expression parsing) +- `vibes/ast.go` (core AST interfaces and shared type nodes) +- `vibes/ast_statements.go` (statement node definitions) +- `vibes/ast_expressions.go` (expression node definitions) +- `vibes/execution_compile.go` (AST lowering into compiled script functions/classes) +- `vibes/execution_compile_functions.go` (function lowering helper for compile) +- `vibes/execution_compile_classes.go` (class/property/method lowering helpers for compile) +- `vibes/execution_compile_errors.go` (parse error aggregation for compile failures) + +## 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 entry/request types and cache access) +- `vibes/modules_load.go` (module load workflows for relative/search-path modules) +- `vibes/modules_paths.go` (module request parsing and path resolution helpers) +- `vibes/modules_policy.go` (module allow/deny policy normalization and enforcement) +- `vibes/modules_compile.go` (module compile/cache helpers and function-env cloning) +- `vibes/modules_cycles.go` (module cycle detection and formatting helpers) +- `vibes/modules_bindings.go` (require alias validation/binding and export helpers) +- `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.go` (JSON parse/stringify builtins) + - `vibes/builtins_json_convert.go` (JSON <-> runtime value conversion helpers) + - `vibes/builtins_json_regex.go` (Regex match builtin) + - `vibes/builtins_regex_replace.go` (Regex replace/replace_all builtins and replacement helpers) +- class/object registration helpers in `vibes/interpreter_builtins_data.go` (`JSON`/`Regex` namespace objects) +- duration class registration in `vibes/interpreter_builtins_duration.go` +- time class registration in `vibes/interpreter_builtins_time.go` + +## Capability Adapters + +Capabilities expose host functionality to scripts through typed contracts and runtime adapters. + +Key files: + +- `vibes/capability_contracts.go` (capability contract declarations and call boundary enforcement) +- `vibes/capability_contracts_cycles.go` (cycle detection scan for capability payloads) +- `vibes/capability_contracts_scanner.go` (callable/builtin scanning and contract binding traversal) +- `vibes/capability_common.go` (shared validation/name helpers and nil-implementation checks) +- `vibes/capability_clone.go` (deep-clone/merge helpers for capability payload isolation) +- `vibes/capability_context.go` (read-only context value capability) +- `vibes/capability_events.go` (event bus capability) +- `vibes/capability_db.go` (database interfaces, request types, and adapter construction) +- `vibes/capability_db_calls.go` (database method binding and runtime call handlers) +- `vibes/capability_db_contracts.go` (database method contracts and argument validation) +- `vibes/capability_jobqueue.go` (job queue interfaces, request types, and adapter construction) +- `vibes/capability_jobqueue_calls.go` (job queue method binding and runtime call handlers) +- `vibes/capability_jobqueue_contracts.go` (job queue method contracts and return validators) +- `vibes/capability_jobqueue_options.go` (job queue enqueue option parsing/coercion helpers) + +## 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. diff --git a/vibes/ast.go b/vibes/ast.go index 42bb405..adaa86c 100644 --- a/vibes/ast.go +++ b/vibes/ast.go @@ -25,20 +25,6 @@ func (p *Program) Pos() Position { return p.Statements[0].Pos() } -type FunctionStmt struct { - Name string - Params []Param - ReturnTy *TypeExpr - Body []Statement - IsClassMethod bool - Exported bool - Private bool - position Position -} - -func (s *FunctionStmt) stmtNode() {} -func (s *FunctionStmt) Pos() Position { return s.position } - type Param struct { Name string Type *TypeExpr @@ -76,328 +62,3 @@ type TypeExpr struct { Union []*TypeExpr position Position } - -type ReturnStmt struct { - Value Expression - position Position -} - -func (s *ReturnStmt) stmtNode() {} -func (s *ReturnStmt) Pos() Position { return s.position } - -type RaiseStmt struct { - Value Expression - position Position -} - -func (s *RaiseStmt) stmtNode() {} -func (s *RaiseStmt) Pos() Position { return s.position } - -type AssignStmt struct { - Target Expression - Value Expression - position Position -} - -func (s *AssignStmt) stmtNode() {} -func (s *AssignStmt) Pos() Position { return s.position } - -type ExprStmt struct { - Expr Expression - position Position -} - -func (s *ExprStmt) stmtNode() {} -func (s *ExprStmt) Pos() Position { return s.position } - -type IfStmt struct { - Condition Expression - Consequent []Statement - ElseIf []*IfStmt - Alternate []Statement - position Position -} - -func (s *IfStmt) stmtNode() {} -func (s *IfStmt) Pos() Position { return s.position } - -type ForStmt struct { - Iterator string - Iterable Expression - Body []Statement - position Position -} - -func (s *ForStmt) stmtNode() {} -func (s *ForStmt) Pos() Position { return s.position } - -type WhileStmt struct { - Condition Expression - Body []Statement - position Position -} - -func (s *WhileStmt) stmtNode() {} -func (s *WhileStmt) Pos() Position { return s.position } - -type UntilStmt struct { - Condition Expression - Body []Statement - position Position -} - -func (s *UntilStmt) stmtNode() {} -func (s *UntilStmt) Pos() Position { return s.position } - -type BreakStmt struct { - position Position -} - -func (s *BreakStmt) stmtNode() {} -func (s *BreakStmt) Pos() Position { return s.position } - -type NextStmt struct { - position Position -} - -func (s *NextStmt) stmtNode() {} -func (s *NextStmt) Pos() Position { return s.position } - -type TryStmt struct { - Body []Statement - RescueTy *TypeExpr - Rescue []Statement - Ensure []Statement - position Position -} - -func (s *TryStmt) stmtNode() {} -func (s *TryStmt) Pos() Position { return s.position } - -type Identifier struct { - Name string - position Position -} - -func (e *Identifier) exprNode() {} -func (e *Identifier) Pos() Position { return e.position } - -type IntegerLiteral struct { - Value int64 - position Position -} - -func (e *IntegerLiteral) exprNode() {} -func (e *IntegerLiteral) Pos() Position { return e.position } - -type FloatLiteral struct { - Value float64 - position Position -} - -func (e *FloatLiteral) exprNode() {} -func (e *FloatLiteral) Pos() Position { return e.position } - -type StringLiteral struct { - Value string - position Position -} - -func (e *StringLiteral) exprNode() {} -func (e *StringLiteral) Pos() Position { return e.position } - -type BoolLiteral struct { - Value bool - position Position -} - -func (e *BoolLiteral) exprNode() {} -func (e *BoolLiteral) Pos() Position { return e.position } - -type NilLiteral struct { - position Position -} - -func (e *NilLiteral) exprNode() {} -func (e *NilLiteral) Pos() Position { return e.position } - -type SymbolLiteral struct { - Name string - position Position -} - -func (e *SymbolLiteral) exprNode() {} -func (e *SymbolLiteral) Pos() Position { return e.position } - -type ArrayLiteral struct { - Elements []Expression - position Position -} - -func (e *ArrayLiteral) exprNode() {} -func (e *ArrayLiteral) Pos() Position { return e.position } - -type HashPair struct { - Key Expression - Value Expression -} - -type HashLiteral struct { - Pairs []HashPair - position Position -} - -func (e *HashLiteral) exprNode() {} -func (e *HashLiteral) Pos() Position { return e.position } - -type CallExpr struct { - Callee Expression - Args []Expression - KwArgs []KeywordArg - Block *BlockLiteral - position Position -} - -func (e *CallExpr) exprNode() {} -func (e *CallExpr) Pos() Position { return e.position } - -type KeywordArg struct { - Name string - Value Expression -} - -type MemberExpr struct { - Object Expression - Property string - position Position -} - -func (e *MemberExpr) exprNode() {} -func (e *MemberExpr) Pos() Position { return e.position } - -type IndexExpr struct { - Object Expression - Index Expression - position Position -} - -type IvarExpr struct { - Name string - position Position -} - -func (e *IvarExpr) exprNode() {} -func (e *IvarExpr) Pos() Position { return e.position } - -type ClassVarExpr struct { - Name string - position Position -} - -func (e *ClassVarExpr) exprNode() {} -func (e *ClassVarExpr) Pos() Position { return e.position } - -func (e *IndexExpr) exprNode() {} -func (e *IndexExpr) Pos() Position { return e.position } - -type UnaryExpr struct { - Operator TokenType - Right Expression - position Position -} - -func (e *UnaryExpr) exprNode() {} -func (e *UnaryExpr) Pos() Position { return e.position } - -type BinaryExpr struct { - Left Expression - Operator TokenType - Right Expression - position Position -} - -func (e *BinaryExpr) exprNode() {} -func (e *BinaryExpr) Pos() Position { return e.position } - -type RangeExpr struct { - Start Expression - End Expression - position Position -} - -func (e *RangeExpr) exprNode() {} -func (e *RangeExpr) Pos() Position { return e.position } - -type CaseWhenClause struct { - Values []Expression - Result Expression -} - -type CaseExpr struct { - Target Expression - Clauses []CaseWhenClause - ElseExpr Expression - position Position -} - -func (e *CaseExpr) exprNode() {} -func (e *CaseExpr) Pos() Position { return e.position } - -type BlockLiteral struct { - Params []Param - Body []Statement - position Position -} - -func (b *BlockLiteral) exprNode() {} -func (b *BlockLiteral) Pos() Position { return b.position } - -type YieldExpr struct { - Args []Expression - position Position -} - -func (y *YieldExpr) exprNode() {} -func (y *YieldExpr) Pos() Position { return y.position } - -type PropertyDecl struct { - Names []string - Kind string // property/getter/setter - position Position -} - -type ClassStmt struct { - Name string - Methods []*FunctionStmt - ClassMethods []*FunctionStmt - Properties []PropertyDecl - Body []Statement - position Position -} - -func (s *ClassStmt) stmtNode() {} -func (s *ClassStmt) Pos() Position { return s.position } - -type InterpolatedString struct { - Parts []StringPart - position Position -} - -type StringPart interface { - isStringPart() -} - -type StringText struct { - Text string -} - -func (StringText) isStringPart() {} - -type StringExpr struct { - Expr Expression -} - -func (StringExpr) isStringPart() {} - -func (s *InterpolatedString) exprNode() {} -func (s *InterpolatedString) Pos() Position { return s.position } diff --git a/vibes/ast_expressions.go b/vibes/ast_expressions.go new file mode 100644 index 0000000..653f964 --- /dev/null +++ b/vibes/ast_expressions.go @@ -0,0 +1,211 @@ +package vibes + +type Identifier struct { + Name string + position Position +} + +func (e *Identifier) exprNode() {} +func (e *Identifier) Pos() Position { return e.position } + +type IntegerLiteral struct { + Value int64 + position Position +} + +func (e *IntegerLiteral) exprNode() {} +func (e *IntegerLiteral) Pos() Position { return e.position } + +type FloatLiteral struct { + Value float64 + position Position +} + +func (e *FloatLiteral) exprNode() {} +func (e *FloatLiteral) Pos() Position { return e.position } + +type StringLiteral struct { + Value string + position Position +} + +func (e *StringLiteral) exprNode() {} +func (e *StringLiteral) Pos() Position { return e.position } + +type BoolLiteral struct { + Value bool + position Position +} + +func (e *BoolLiteral) exprNode() {} +func (e *BoolLiteral) Pos() Position { return e.position } + +type NilLiteral struct { + position Position +} + +func (e *NilLiteral) exprNode() {} +func (e *NilLiteral) Pos() Position { return e.position } + +type SymbolLiteral struct { + Name string + position Position +} + +func (e *SymbolLiteral) exprNode() {} +func (e *SymbolLiteral) Pos() Position { return e.position } + +type ArrayLiteral struct { + Elements []Expression + position Position +} + +func (e *ArrayLiteral) exprNode() {} +func (e *ArrayLiteral) Pos() Position { return e.position } + +type HashPair struct { + Key Expression + Value Expression +} + +type HashLiteral struct { + Pairs []HashPair + position Position +} + +func (e *HashLiteral) exprNode() {} +func (e *HashLiteral) Pos() Position { return e.position } + +type CallExpr struct { + Callee Expression + Args []Expression + KwArgs []KeywordArg + Block *BlockLiteral + position Position +} + +func (e *CallExpr) exprNode() {} +func (e *CallExpr) Pos() Position { return e.position } + +type KeywordArg struct { + Name string + Value Expression +} + +type MemberExpr struct { + Object Expression + Property string + position Position +} + +func (e *MemberExpr) exprNode() {} +func (e *MemberExpr) Pos() Position { return e.position } + +type IndexExpr struct { + Object Expression + Index Expression + position Position +} + +func (e *IndexExpr) exprNode() {} +func (e *IndexExpr) Pos() Position { return e.position } + +type IvarExpr struct { + Name string + position Position +} + +func (e *IvarExpr) exprNode() {} +func (e *IvarExpr) Pos() Position { return e.position } + +type ClassVarExpr struct { + Name string + position Position +} + +func (e *ClassVarExpr) exprNode() {} +func (e *ClassVarExpr) Pos() Position { return e.position } + +type UnaryExpr struct { + Operator TokenType + Right Expression + position Position +} + +func (e *UnaryExpr) exprNode() {} +func (e *UnaryExpr) Pos() Position { return e.position } + +type BinaryExpr struct { + Left Expression + Operator TokenType + Right Expression + position Position +} + +func (e *BinaryExpr) exprNode() {} +func (e *BinaryExpr) Pos() Position { return e.position } + +type RangeExpr struct { + Start Expression + End Expression + position Position +} + +func (e *RangeExpr) exprNode() {} +func (e *RangeExpr) Pos() Position { return e.position } + +type CaseWhenClause struct { + Values []Expression + Result Expression +} + +type CaseExpr struct { + Target Expression + Clauses []CaseWhenClause + ElseExpr Expression + position Position +} + +func (e *CaseExpr) exprNode() {} +func (e *CaseExpr) Pos() Position { return e.position } + +type BlockLiteral struct { + Params []Param + Body []Statement + position Position +} + +func (b *BlockLiteral) exprNode() {} +func (b *BlockLiteral) Pos() Position { return b.position } + +type YieldExpr struct { + Args []Expression + position Position +} + +func (y *YieldExpr) exprNode() {} +func (y *YieldExpr) Pos() Position { return y.position } + +type InterpolatedString struct { + Parts []StringPart + position Position +} + +type StringPart interface { + isStringPart() +} + +type StringText struct { + Text string +} + +func (StringText) isStringPart() {} + +type StringExpr struct { + Expr Expression +} + +func (StringExpr) isStringPart() {} + +func (s *InterpolatedString) exprNode() {} +func (s *InterpolatedString) Pos() Position { return s.position } diff --git a/vibes/ast_statements.go b/vibes/ast_statements.go new file mode 100644 index 0000000..6bcfb25 --- /dev/null +++ b/vibes/ast_statements.go @@ -0,0 +1,130 @@ +package vibes + +type FunctionStmt struct { + Name string + Params []Param + ReturnTy *TypeExpr + Body []Statement + IsClassMethod bool + Exported bool + Private bool + position Position +} + +func (s *FunctionStmt) stmtNode() {} +func (s *FunctionStmt) Pos() Position { return s.position } + +type ReturnStmt struct { + Value Expression + position Position +} + +func (s *ReturnStmt) stmtNode() {} +func (s *ReturnStmt) Pos() Position { return s.position } + +type RaiseStmt struct { + Value Expression + position Position +} + +func (s *RaiseStmt) stmtNode() {} +func (s *RaiseStmt) Pos() Position { return s.position } + +type AssignStmt struct { + Target Expression + Value Expression + position Position +} + +func (s *AssignStmt) stmtNode() {} +func (s *AssignStmt) Pos() Position { return s.position } + +type ExprStmt struct { + Expr Expression + position Position +} + +func (s *ExprStmt) stmtNode() {} +func (s *ExprStmt) Pos() Position { return s.position } + +type IfStmt struct { + Condition Expression + Consequent []Statement + ElseIf []*IfStmt + Alternate []Statement + position Position +} + +func (s *IfStmt) stmtNode() {} +func (s *IfStmt) Pos() Position { return s.position } + +type ForStmt struct { + Iterator string + Iterable Expression + Body []Statement + position Position +} + +func (s *ForStmt) stmtNode() {} +func (s *ForStmt) Pos() Position { return s.position } + +type WhileStmt struct { + Condition Expression + Body []Statement + position Position +} + +func (s *WhileStmt) stmtNode() {} +func (s *WhileStmt) Pos() Position { return s.position } + +type UntilStmt struct { + Condition Expression + Body []Statement + position Position +} + +func (s *UntilStmt) stmtNode() {} +func (s *UntilStmt) Pos() Position { return s.position } + +type BreakStmt struct { + position Position +} + +func (s *BreakStmt) stmtNode() {} +func (s *BreakStmt) Pos() Position { return s.position } + +type NextStmt struct { + position Position +} + +func (s *NextStmt) stmtNode() {} +func (s *NextStmt) Pos() Position { return s.position } + +type TryStmt struct { + Body []Statement + RescueTy *TypeExpr + Rescue []Statement + Ensure []Statement + position Position +} + +func (s *TryStmt) stmtNode() {} +func (s *TryStmt) Pos() Position { return s.position } + +type PropertyDecl struct { + Names []string + Kind string // property/getter/setter + position Position +} + +type ClassStmt struct { + Name string + Methods []*FunctionStmt + ClassMethods []*FunctionStmt + Properties []PropertyDecl + Body []Statement + position Position +} + +func (s *ClassStmt) stmtNode() {} +func (s *ClassStmt) Pos() Position { return s.position } 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.go b/vibes/builtins_json.go new file mode 100644 index 0000000..02829a7 --- /dev/null +++ b/vibes/builtins_json.go @@ -0,0 +1,72 @@ +package vibes + +import ( + "encoding/json" + "fmt" + "io" + "strings" +) + +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 +} diff --git a/vibes/builtins_json_convert.go b/vibes/builtins_json_convert.go new file mode 100644 index 0000000..783a497 --- /dev/null +++ b/vibes/builtins_json_convert.go @@ -0,0 +1,108 @@ +package vibes + +import ( + "encoding/json" + "fmt" + "reflect" +) + +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()) + } +} diff --git a/vibes/builtins_json_regex.go b/vibes/builtins_json_regex.go new file mode 100644 index 0000000..4a90b34 --- /dev/null +++ b/vibes/builtins_json_regex.go @@ -0,0 +1,39 @@ +package vibes + +import ( + "fmt" + "regexp" +) + +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 +} 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/builtins_regex_replace.go b/vibes/builtins_regex_replace.go new file mode 100644 index 0000000..a3e8d08 --- /dev/null +++ b/vibes/builtins_regex_replace.go @@ -0,0 +1,173 @@ +package vibes + +import ( + "fmt" + "regexp" + "unicode/utf8" +) + +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/capability_clone.go b/vibes/capability_clone.go new file mode 100644 index 0000000..0be56d6 --- /dev/null +++ b/vibes/capability_clone.go @@ -0,0 +1,53 @@ +package vibes + +import "maps" + +func cloneHash(src map[string]Value) map[string]Value { + if len(src) == 0 { + return map[string]Value{} + } + out := make(map[string]Value, len(src)) + for k, v := range src { + out[k] = deepCloneValue(v) + } + return out +} + +func deepCloneValue(val Value) Value { + switch val.Kind() { + case KindArray: + arr := val.Array() + cloned := make([]Value, len(arr)) + for i, elem := range arr { + cloned[i] = deepCloneValue(elem) + } + return NewArray(cloned) + case KindHash: + hash := val.Hash() + cloned := make(map[string]Value, len(hash)) + for k, v := range hash { + cloned[k] = deepCloneValue(v) + } + return NewHash(cloned) + case KindObject: + obj := val.Hash() + cloned := make(map[string]Value, len(obj)) + for k, v := range obj { + cloned[k] = deepCloneValue(v) + } + return NewObject(cloned) + default: + return val + } +} + +func mergeHash(dest map[string]Value, src map[string]Value) map[string]Value { + if len(src) == 0 { + return dest + } + if dest == nil { + dest = make(map[string]Value, len(src)) + } + maps.Copy(dest, src) + return dest +} diff --git a/vibes/capability_common.go b/vibes/capability_common.go index b05825c..cc8c97e 100644 --- a/vibes/capability_common.go +++ b/vibes/capability_common.go @@ -65,6 +65,19 @@ func validateCapabilityHashValue(label string, val Value) error { return validateCapabilityTypedValue(label, val, capabilityTypeHash) } +func capabilityValidateAnyReturn(method string) func(result Value) error { + return func(result Value) error { + return validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny) + } +} + +func cloneCapabilityMethodResult(method string, result Value) (Value, error) { + if err := validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny); err != nil { + return NewNil(), err + } + return deepCloneValue(result), nil +} + func isNilCapabilityImplementation(impl any) bool { if impl == nil { return true diff --git a/vibes/capability_contracts.go b/vibes/capability_contracts.go index 1859666..e40d57f 100644 --- a/vibes/capability_contracts.go +++ b/vibes/capability_contracts.go @@ -2,8 +2,6 @@ package vibes import ( "fmt" - "reflect" - "slices" ) type capabilityContractScanner struct { @@ -35,69 +33,6 @@ func validateCapabilityDataOnlyValue(label string, val Value) error { return nil } -type capabilityCycleScanner struct { - visitingArrays map[sliceIdentity]struct{} - visitingMaps map[uintptr]struct{} - seenArrays map[sliceIdentity]struct{} - seenMaps map[uintptr]struct{} -} - -func newCapabilityCycleScanner() *capabilityCycleScanner { - return &capabilityCycleScanner{ - visitingArrays: make(map[sliceIdentity]struct{}), - visitingMaps: make(map[uintptr]struct{}), - seenArrays: make(map[sliceIdentity]struct{}), - seenMaps: make(map[uintptr]struct{}), - } -} - -func (s *capabilityCycleScanner) containsCycle(val Value) bool { - switch val.Kind() { - case KindArray: - values := val.Array() - id := sliceIdentity{ - ptr: reflect.ValueOf(values).Pointer(), - len: len(values), - cap: cap(values), - } - if _, seen := s.seenArrays[id]; seen { - return false - } - if _, visiting := s.visitingArrays[id]; visiting { - return true - } - s.visitingArrays[id] = struct{}{} - for _, item := range values { - if s.containsCycle(item) { - return true - } - } - delete(s.visitingArrays, id) - s.seenArrays[id] = struct{}{} - return false - case KindHash, KindObject: - entries := val.Hash() - ptr := reflect.ValueOf(entries).Pointer() - if _, seen := s.seenMaps[ptr]; seen { - return false - } - if _, visiting := s.visitingMaps[ptr]; visiting { - return true - } - s.visitingMaps[ptr] = struct{}{} - for _, item := range entries { - if s.containsCycle(item) { - return true - } - } - delete(s.visitingMaps, ptr) - s.seenMaps[ptr] = struct{}{} - return false - default: - return false - } -} - func bindCapabilityContracts( val Value, scope *capabilityContractScope, @@ -129,174 +64,3 @@ func collectCapabilityBuiltins(val Value, out map[*Builtin]struct{}) { scanner := newCapabilityContractScanner() scanner.collectBuiltins(val, out) } - -func (s *capabilityContractScanner) containsCallable(val Value) bool { - switch val.Kind() { - case KindFunction, KindBuiltin, KindBlock, KindClass, KindInstance: - return true - case KindArray: - values := val.Array() - id := sliceIdentity{ - ptr: reflect.ValueOf(values).Pointer(), - len: len(values), - cap: cap(values), - } - if _, seen := s.seenArrays[id]; seen { - return false - } - s.seenArrays[id] = struct{}{} - return slices.ContainsFunc(values, s.containsCallable) - case KindHash, KindObject: - entries := val.Hash() - ptr := reflect.ValueOf(entries).Pointer() - if _, seen := s.seenMaps[ptr]; seen { - return false - } - s.seenMaps[ptr] = struct{}{} - for _, item := range entries { - if s.containsCallable(item) { - return true - } - } - return false - default: - return false - } -} - -func (s *capabilityContractScanner) bindContracts( - val Value, - scope *capabilityContractScope, - target map[*Builtin]CapabilityMethodContract, - scopes map[*Builtin]*capabilityContractScope, -) { - switch val.Kind() { - case KindBuiltin: - builtin := val.Builtin() - if _, skip := s.excluded[builtin]; skip { - return - } - ownerScope, seen := scopes[builtin] - if !seen { - scopes[builtin] = scope - ownerScope = scope - } - if ownerScope != scope { - return - } - if contract, ok := scope.contracts[builtin.Name]; ok { - if _, seen := target[builtin]; !seen { - target[builtin] = contract - } - } - case KindArray: - values := val.Array() - id := sliceIdentity{ - ptr: reflect.ValueOf(values).Pointer(), - len: len(values), - cap: cap(values), - } - if _, seen := s.seenArrays[id]; seen { - return - } - s.seenArrays[id] = struct{}{} - for _, item := range values { - s.bindContracts(item, scope, target, scopes) - } - case KindHash, KindObject: - entries := val.Hash() - ptr := reflect.ValueOf(entries).Pointer() - if _, seen := s.seenMaps[ptr]; seen { - return - } - s.seenMaps[ptr] = struct{}{} - for _, item := range entries { - s.bindContracts(item, scope, target, scopes) - } - case KindClass: - classDef := val.Class() - if classDef == nil { - return - } - if _, seen := s.seenClasses[classDef]; seen { - return - } - s.seenClasses[classDef] = struct{}{} - for _, item := range classDef.ClassVars { - s.bindContracts(item, scope, target, scopes) - } - case KindInstance: - instance := val.Instance() - if instance == nil { - return - } - if _, seen := s.seenInstances[instance]; seen { - return - } - s.seenInstances[instance] = struct{}{} - for _, item := range instance.Ivars { - s.bindContracts(item, scope, target, scopes) - } - if instance.Class != nil { - s.bindContracts(NewClass(instance.Class), scope, target, scopes) - } - } -} - -func (s *capabilityContractScanner) collectBuiltins(val Value, out map[*Builtin]struct{}) { - switch val.Kind() { - case KindBuiltin: - out[val.Builtin()] = struct{}{} - case KindArray: - values := val.Array() - id := sliceIdentity{ - ptr: reflect.ValueOf(values).Pointer(), - len: len(values), - cap: cap(values), - } - if _, seen := s.seenArrays[id]; seen { - return - } - s.seenArrays[id] = struct{}{} - for _, item := range values { - s.collectBuiltins(item, out) - } - case KindHash, KindObject: - entries := val.Hash() - ptr := reflect.ValueOf(entries).Pointer() - if _, seen := s.seenMaps[ptr]; seen { - return - } - s.seenMaps[ptr] = struct{}{} - for _, item := range entries { - s.collectBuiltins(item, out) - } - case KindClass: - classDef := val.Class() - if classDef == nil { - return - } - if _, seen := s.seenClasses[classDef]; seen { - return - } - s.seenClasses[classDef] = struct{}{} - for _, item := range classDef.ClassVars { - s.collectBuiltins(item, out) - } - case KindInstance: - instance := val.Instance() - if instance == nil { - return - } - if _, seen := s.seenInstances[instance]; seen { - return - } - s.seenInstances[instance] = struct{}{} - for _, item := range instance.Ivars { - s.collectBuiltins(item, out) - } - if instance.Class != nil { - s.collectBuiltins(NewClass(instance.Class), out) - } - } -} diff --git a/vibes/capability_contracts_cycles.go b/vibes/capability_contracts_cycles.go new file mode 100644 index 0000000..f8c5e4e --- /dev/null +++ b/vibes/capability_contracts_cycles.go @@ -0,0 +1,66 @@ +package vibes + +import "reflect" + +type capabilityCycleScanner struct { + visitingArrays map[sliceIdentity]struct{} + visitingMaps map[uintptr]struct{} + seenArrays map[sliceIdentity]struct{} + seenMaps map[uintptr]struct{} +} + +func newCapabilityCycleScanner() *capabilityCycleScanner { + return &capabilityCycleScanner{ + visitingArrays: make(map[sliceIdentity]struct{}), + visitingMaps: make(map[uintptr]struct{}), + seenArrays: make(map[sliceIdentity]struct{}), + seenMaps: make(map[uintptr]struct{}), + } +} + +func (s *capabilityCycleScanner) containsCycle(val Value) bool { + switch val.Kind() { + case KindArray: + values := val.Array() + id := sliceIdentity{ + ptr: reflect.ValueOf(values).Pointer(), + len: len(values), + cap: cap(values), + } + if _, seen := s.seenArrays[id]; seen { + return false + } + if _, visiting := s.visitingArrays[id]; visiting { + return true + } + s.visitingArrays[id] = struct{}{} + for _, item := range values { + if s.containsCycle(item) { + return true + } + } + delete(s.visitingArrays, id) + s.seenArrays[id] = struct{}{} + return false + case KindHash, KindObject: + entries := val.Hash() + ptr := reflect.ValueOf(entries).Pointer() + if _, seen := s.seenMaps[ptr]; seen { + return false + } + if _, visiting := s.visitingMaps[ptr]; visiting { + return true + } + s.visitingMaps[ptr] = struct{}{} + for _, item := range entries { + if s.containsCycle(item) { + return true + } + } + delete(s.visitingMaps, ptr) + s.seenMaps[ptr] = struct{}{} + return false + default: + return false + } +} diff --git a/vibes/capability_contracts_scanner.go b/vibes/capability_contracts_scanner.go new file mode 100644 index 0000000..2b6bc14 --- /dev/null +++ b/vibes/capability_contracts_scanner.go @@ -0,0 +1,177 @@ +package vibes + +import ( + "reflect" + "slices" +) + +func (s *capabilityContractScanner) containsCallable(val Value) bool { + switch val.Kind() { + case KindFunction, KindBuiltin, KindBlock, KindClass, KindInstance: + return true + case KindArray: + values := val.Array() + id := sliceIdentity{ + ptr: reflect.ValueOf(values).Pointer(), + len: len(values), + cap: cap(values), + } + if _, seen := s.seenArrays[id]; seen { + return false + } + s.seenArrays[id] = struct{}{} + return slices.ContainsFunc(values, s.containsCallable) + case KindHash, KindObject: + entries := val.Hash() + ptr := reflect.ValueOf(entries).Pointer() + if _, seen := s.seenMaps[ptr]; seen { + return false + } + s.seenMaps[ptr] = struct{}{} + for _, item := range entries { + if s.containsCallable(item) { + return true + } + } + return false + default: + return false + } +} + +func (s *capabilityContractScanner) bindContracts( + val Value, + scope *capabilityContractScope, + target map[*Builtin]CapabilityMethodContract, + scopes map[*Builtin]*capabilityContractScope, +) { + switch val.Kind() { + case KindBuiltin: + builtin := val.Builtin() + if _, skip := s.excluded[builtin]; skip { + return + } + ownerScope, seen := scopes[builtin] + if !seen { + scopes[builtin] = scope + ownerScope = scope + } + if ownerScope != scope { + return + } + if contract, ok := scope.contracts[builtin.Name]; ok { + if _, seen := target[builtin]; !seen { + target[builtin] = contract + } + } + case KindArray: + values := val.Array() + id := sliceIdentity{ + ptr: reflect.ValueOf(values).Pointer(), + len: len(values), + cap: cap(values), + } + if _, seen := s.seenArrays[id]; seen { + return + } + s.seenArrays[id] = struct{}{} + for _, item := range values { + s.bindContracts(item, scope, target, scopes) + } + case KindHash, KindObject: + entries := val.Hash() + ptr := reflect.ValueOf(entries).Pointer() + if _, seen := s.seenMaps[ptr]; seen { + return + } + s.seenMaps[ptr] = struct{}{} + for _, item := range entries { + s.bindContracts(item, scope, target, scopes) + } + case KindClass: + classDef := val.Class() + if classDef == nil { + return + } + if _, seen := s.seenClasses[classDef]; seen { + return + } + s.seenClasses[classDef] = struct{}{} + for _, item := range classDef.ClassVars { + s.bindContracts(item, scope, target, scopes) + } + case KindInstance: + instance := val.Instance() + if instance == nil { + return + } + if _, seen := s.seenInstances[instance]; seen { + return + } + s.seenInstances[instance] = struct{}{} + for _, item := range instance.Ivars { + s.bindContracts(item, scope, target, scopes) + } + if instance.Class != nil { + s.bindContracts(NewClass(instance.Class), scope, target, scopes) + } + } +} + +func (s *capabilityContractScanner) collectBuiltins(val Value, out map[*Builtin]struct{}) { + switch val.Kind() { + case KindBuiltin: + out[val.Builtin()] = struct{}{} + case KindArray: + values := val.Array() + id := sliceIdentity{ + ptr: reflect.ValueOf(values).Pointer(), + len: len(values), + cap: cap(values), + } + if _, seen := s.seenArrays[id]; seen { + return + } + s.seenArrays[id] = struct{}{} + for _, item := range values { + s.collectBuiltins(item, out) + } + case KindHash, KindObject: + entries := val.Hash() + ptr := reflect.ValueOf(entries).Pointer() + if _, seen := s.seenMaps[ptr]; seen { + return + } + s.seenMaps[ptr] = struct{}{} + for _, item := range entries { + s.collectBuiltins(item, out) + } + case KindClass: + classDef := val.Class() + if classDef == nil { + return + } + if _, seen := s.seenClasses[classDef]; seen { + return + } + s.seenClasses[classDef] = struct{}{} + for _, item := range classDef.ClassVars { + s.collectBuiltins(item, out) + } + case KindInstance: + instance := val.Instance() + if instance == nil { + return + } + if _, seen := s.seenInstances[instance]; seen { + return + } + s.seenInstances[instance] = struct{}{} + for _, item := range instance.Ivars { + s.collectBuiltins(item, out) + } + if instance.Class != nil { + s.collectBuiltins(NewClass(instance.Class), out) + } + } +} diff --git a/vibes/capability_db.go b/vibes/capability_db.go index 3f8a709..1324e9e 100644 --- a/vibes/capability_db.go +++ b/vibes/capability_db.go @@ -72,230 +72,3 @@ type dbCapability struct { name string db Database } - -func (c *dbCapability) CapabilityContracts() map[string]CapabilityMethodContract { - return map[string]CapabilityMethodContract{ - c.name + ".find": { - ValidateArgs: c.validateFindContractArgs, - ValidateReturn: c.validateMethodReturn(c.name + ".find"), - }, - c.name + ".query": { - ValidateArgs: c.validateQueryContractArgs, - ValidateReturn: c.validateMethodReturn(c.name + ".query"), - }, - c.name + ".update": { - ValidateArgs: c.validateUpdateContractArgs, - ValidateReturn: c.validateMethodReturn(c.name + ".update"), - }, - c.name + ".sum": { - ValidateArgs: c.validateSumContractArgs, - ValidateReturn: c.validateMethodReturn(c.name + ".sum"), - }, - c.name + ".each": { - ValidateArgs: c.validateEachContractArgs, - ValidateReturn: c.validateMethodReturn(c.name + ".each"), - }, - } -} - -func (c *dbCapability) Bind(binding CapabilityBinding) (map[string]Value, error) { - methods := map[string]Value{ - "find": NewBuiltin(c.name+".find", c.callFind), - "query": NewBuiltin(c.name+".query", c.callQuery), - "update": NewBuiltin(c.name+".update", c.callUpdate), - "sum": NewBuiltin(c.name+".sum", c.callSum), - "each": NewBuiltin(c.name+".each", c.callEach), - } - return map[string]Value{c.name: NewObject(methods)}, nil -} - -func (c *dbCapability) callFind(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := c.validateFindContractArgs(args, kwargs, block); err != nil { - return NewNil(), err - } - collection, _ := capabilityNameArg(c.name+".find", "collection", args[0]) - req := DBFindRequest{ - Collection: collection, - ID: deepCloneValue(args[1]), - Options: cloneCapabilityKwargs(kwargs), - } - result, err := c.db.Find(exec.ctx, req) - if err != nil { - return NewNil(), err - } - return c.cloneMethodResult(c.name+".find", result) -} - -func (c *dbCapability) callQuery(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := c.validateQueryContractArgs(args, kwargs, block); err != nil { - return NewNil(), err - } - collection, _ := capabilityNameArg(c.name+".query", "collection", args[0]) - req := DBQueryRequest{ - Collection: collection, - Options: cloneCapabilityKwargs(kwargs), - } - result, err := c.db.Query(exec.ctx, req) - if err != nil { - return NewNil(), err - } - return c.cloneMethodResult(c.name+".query", result) -} - -func (c *dbCapability) callUpdate(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := c.validateUpdateContractArgs(args, kwargs, block); err != nil { - return NewNil(), err - } - collection, _ := capabilityNameArg(c.name+".update", "collection", args[0]) - req := DBUpdateRequest{ - Collection: collection, - ID: deepCloneValue(args[1]), - Attributes: cloneHash(args[2].Hash()), - Options: cloneCapabilityKwargs(kwargs), - } - result, err := c.db.Update(exec.ctx, req) - if err != nil { - return NewNil(), err - } - return c.cloneMethodResult(c.name+".update", result) -} - -func (c *dbCapability) callSum(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := c.validateSumContractArgs(args, kwargs, block); err != nil { - return NewNil(), err - } - collection, _ := capabilityNameArg(c.name+".sum", "collection", args[0]) - field, _ := capabilityNameArg(c.name+".sum", "field", args[1]) - req := DBSumRequest{ - Collection: collection, - Field: field, - Options: cloneCapabilityKwargs(kwargs), - } - result, err := c.db.Sum(exec.ctx, req) - if err != nil { - return NewNil(), err - } - return c.cloneMethodResult(c.name+".sum", result) -} - -func (c *dbCapability) callEach(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if err := c.validateEachContractArgs(args, kwargs, block); err != nil { - return NewNil(), err - } - collection, _ := capabilityNameArg(c.name+".each", "collection", args[0]) - req := DBEachRequest{ - Collection: collection, - Options: cloneCapabilityKwargs(kwargs), - } - rows, err := c.db.Each(exec.ctx, req) - if err != nil { - return NewNil(), err - } - for idx, row := range rows { - if err := exec.step(); err != nil { - return NewNil(), err - } - if err := validateCapabilityTypedValue(fmt.Sprintf("%s.each row %d", c.name, idx), row, capabilityTypeAny); err != nil { - return NewNil(), err - } - if _, err := exec.CallBlock(block, []Value{deepCloneValue(row)}); err != nil { - return NewNil(), err - } - } - return NewNil(), nil -} - -func (c *dbCapability) validateFindContractArgs(args []Value, kwargs map[string]Value, block Value) error { - method := c.name + ".find" - if len(args) != 2 { - return fmt.Errorf("%s expects collection and id", method) - } - if !block.IsNil() { - return fmt.Errorf("%s does not accept blocks", method) - } - if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { - return err - } - if err := validateCapabilityTypedValue(method+" id", args[1], capabilityTypeAny); err != nil { - return err - } - return validateCapabilityKwargsDataOnly(method, kwargs) -} - -func (c *dbCapability) validateQueryContractArgs(args []Value, kwargs map[string]Value, block Value) error { - method := c.name + ".query" - if len(args) != 1 { - return fmt.Errorf("%s expects collection", method) - } - if !block.IsNil() { - return fmt.Errorf("%s does not accept blocks", method) - } - if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { - return err - } - return validateCapabilityKwargsDataOnly(method, kwargs) -} - -func (c *dbCapability) validateUpdateContractArgs(args []Value, kwargs map[string]Value, block Value) error { - method := c.name + ".update" - if len(args) != 3 { - return fmt.Errorf("%s expects collection, id, and attributes", method) - } - if !block.IsNil() { - return fmt.Errorf("%s does not accept blocks", method) - } - if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { - return err - } - if err := validateCapabilityTypedValue(method+" id", args[1], capabilityTypeAny); err != nil { - return err - } - if err := validateCapabilityHashValue(method+" attributes", args[2]); err != nil { - return err - } - return validateCapabilityKwargsDataOnly(method, kwargs) -} - -func (c *dbCapability) validateSumContractArgs(args []Value, kwargs map[string]Value, block Value) error { - method := c.name + ".sum" - if len(args) != 2 { - return fmt.Errorf("%s expects collection and field", method) - } - if !block.IsNil() { - return fmt.Errorf("%s does not accept blocks", method) - } - if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { - return err - } - if _, err := capabilityNameArg(method, "field", args[1]); err != nil { - return err - } - return validateCapabilityKwargsDataOnly(method, kwargs) -} - -func (c *dbCapability) validateEachContractArgs(args []Value, kwargs map[string]Value, block Value) error { - method := c.name + ".each" - if len(args) != 1 { - return fmt.Errorf("%s expects collection", method) - } - if err := ensureBlock(block, method); err != nil { - return err - } - if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { - return err - } - return validateCapabilityKwargsDataOnly(method, kwargs) -} - -func (c *dbCapability) validateMethodReturn(method string) func(result Value) error { - return func(result Value) error { - return validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny) - } -} - -func (c *dbCapability) cloneMethodResult(method string, result Value) (Value, error) { - if err := validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny); err != nil { - return NewNil(), err - } - return deepCloneValue(result), nil -} diff --git a/vibes/capability_db_calls.go b/vibes/capability_db_calls.go new file mode 100644 index 0000000..bf142ee --- /dev/null +++ b/vibes/capability_db_calls.go @@ -0,0 +1,110 @@ +package vibes + +import "fmt" + +func (c *dbCapability) Bind(binding CapabilityBinding) (map[string]Value, error) { + methods := map[string]Value{ + "find": NewBuiltin(c.name+".find", c.callFind), + "query": NewBuiltin(c.name+".query", c.callQuery), + "update": NewBuiltin(c.name+".update", c.callUpdate), + "sum": NewBuiltin(c.name+".sum", c.callSum), + "each": NewBuiltin(c.name+".each", c.callEach), + } + return map[string]Value{c.name: NewObject(methods)}, nil +} + +func (c *dbCapability) callFind(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := c.validateFindContractArgs(args, kwargs, block); err != nil { + return NewNil(), err + } + collection, _ := capabilityNameArg(c.name+".find", "collection", args[0]) + req := DBFindRequest{ + Collection: collection, + ID: deepCloneValue(args[1]), + Options: cloneCapabilityKwargs(kwargs), + } + result, err := c.db.Find(exec.ctx, req) + if err != nil { + return NewNil(), err + } + return cloneCapabilityMethodResult(c.name+".find", result) +} + +func (c *dbCapability) callQuery(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := c.validateQueryContractArgs(args, kwargs, block); err != nil { + return NewNil(), err + } + collection, _ := capabilityNameArg(c.name+".query", "collection", args[0]) + req := DBQueryRequest{ + Collection: collection, + Options: cloneCapabilityKwargs(kwargs), + } + result, err := c.db.Query(exec.ctx, req) + if err != nil { + return NewNil(), err + } + return cloneCapabilityMethodResult(c.name+".query", result) +} + +func (c *dbCapability) callUpdate(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := c.validateUpdateContractArgs(args, kwargs, block); err != nil { + return NewNil(), err + } + collection, _ := capabilityNameArg(c.name+".update", "collection", args[0]) + req := DBUpdateRequest{ + Collection: collection, + ID: deepCloneValue(args[1]), + Attributes: cloneHash(args[2].Hash()), + Options: cloneCapabilityKwargs(kwargs), + } + result, err := c.db.Update(exec.ctx, req) + if err != nil { + return NewNil(), err + } + return cloneCapabilityMethodResult(c.name+".update", result) +} + +func (c *dbCapability) callSum(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := c.validateSumContractArgs(args, kwargs, block); err != nil { + return NewNil(), err + } + collection, _ := capabilityNameArg(c.name+".sum", "collection", args[0]) + field, _ := capabilityNameArg(c.name+".sum", "field", args[1]) + req := DBSumRequest{ + Collection: collection, + Field: field, + Options: cloneCapabilityKwargs(kwargs), + } + result, err := c.db.Sum(exec.ctx, req) + if err != nil { + return NewNil(), err + } + return cloneCapabilityMethodResult(c.name+".sum", result) +} + +func (c *dbCapability) callEach(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if err := c.validateEachContractArgs(args, kwargs, block); err != nil { + return NewNil(), err + } + collection, _ := capabilityNameArg(c.name+".each", "collection", args[0]) + req := DBEachRequest{ + Collection: collection, + Options: cloneCapabilityKwargs(kwargs), + } + rows, err := c.db.Each(exec.ctx, req) + if err != nil { + return NewNil(), err + } + for idx, row := range rows { + if err := exec.step(); err != nil { + return NewNil(), err + } + if err := validateCapabilityTypedValue(fmt.Sprintf("%s.each row %d", c.name, idx), row, capabilityTypeAny); err != nil { + return NewNil(), err + } + if _, err := exec.CallBlock(block, []Value{deepCloneValue(row)}); err != nil { + return NewNil(), err + } + } + return NewNil(), nil +} diff --git a/vibes/capability_db_contracts.go b/vibes/capability_db_contracts.go new file mode 100644 index 0000000..cd2cf12 --- /dev/null +++ b/vibes/capability_db_contracts.go @@ -0,0 +1,110 @@ +package vibes + +import "fmt" + +func (c *dbCapability) CapabilityContracts() map[string]CapabilityMethodContract { + return map[string]CapabilityMethodContract{ + c.name + ".find": { + ValidateArgs: c.validateFindContractArgs, + ValidateReturn: capabilityValidateAnyReturn(c.name + ".find"), + }, + c.name + ".query": { + ValidateArgs: c.validateQueryContractArgs, + ValidateReturn: capabilityValidateAnyReturn(c.name + ".query"), + }, + c.name + ".update": { + ValidateArgs: c.validateUpdateContractArgs, + ValidateReturn: capabilityValidateAnyReturn(c.name + ".update"), + }, + c.name + ".sum": { + ValidateArgs: c.validateSumContractArgs, + ValidateReturn: capabilityValidateAnyReturn(c.name + ".sum"), + }, + c.name + ".each": { + ValidateArgs: c.validateEachContractArgs, + ValidateReturn: capabilityValidateAnyReturn(c.name + ".each"), + }, + } +} + +func (c *dbCapability) validateFindContractArgs(args []Value, kwargs map[string]Value, block Value) error { + method := c.name + ".find" + if len(args) != 2 { + return fmt.Errorf("%s expects collection and id", method) + } + if !block.IsNil() { + return fmt.Errorf("%s does not accept blocks", method) + } + if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { + return err + } + if err := validateCapabilityTypedValue(method+" id", args[1], capabilityTypeAny); err != nil { + return err + } + return validateCapabilityKwargsDataOnly(method, kwargs) +} + +func (c *dbCapability) validateQueryContractArgs(args []Value, kwargs map[string]Value, block Value) error { + method := c.name + ".query" + if len(args) != 1 { + return fmt.Errorf("%s expects collection", method) + } + if !block.IsNil() { + return fmt.Errorf("%s does not accept blocks", method) + } + if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { + return err + } + return validateCapabilityKwargsDataOnly(method, kwargs) +} + +func (c *dbCapability) validateUpdateContractArgs(args []Value, kwargs map[string]Value, block Value) error { + method := c.name + ".update" + if len(args) != 3 { + return fmt.Errorf("%s expects collection, id, and attributes", method) + } + if !block.IsNil() { + return fmt.Errorf("%s does not accept blocks", method) + } + if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { + return err + } + if err := validateCapabilityTypedValue(method+" id", args[1], capabilityTypeAny); err != nil { + return err + } + if err := validateCapabilityHashValue(method+" attributes", args[2]); err != nil { + return err + } + return validateCapabilityKwargsDataOnly(method, kwargs) +} + +func (c *dbCapability) validateSumContractArgs(args []Value, kwargs map[string]Value, block Value) error { + method := c.name + ".sum" + if len(args) != 2 { + return fmt.Errorf("%s expects collection and field", method) + } + if !block.IsNil() { + return fmt.Errorf("%s does not accept blocks", method) + } + if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { + return err + } + if _, err := capabilityNameArg(method, "field", args[1]); err != nil { + return err + } + return validateCapabilityKwargsDataOnly(method, kwargs) +} + +func (c *dbCapability) validateEachContractArgs(args []Value, kwargs map[string]Value, block Value) error { + method := c.name + ".each" + if len(args) != 1 { + return fmt.Errorf("%s expects collection", method) + } + if err := ensureBlock(block, method); err != nil { + return err + } + if _, err := capabilityNameArg(method, "collection", args[0]); err != nil { + return err + } + return validateCapabilityKwargsDataOnly(method, kwargs) +} diff --git a/vibes/capability_events.go b/vibes/capability_events.go index e64c835..8df3645 100644 --- a/vibes/capability_events.go +++ b/vibes/capability_events.go @@ -47,7 +47,7 @@ func (c *eventsCapability) CapabilityContracts() map[string]CapabilityMethodCont return map[string]CapabilityMethodContract{ method: { ValidateArgs: c.validatePublishContractArgs, - ValidateReturn: c.validateMethodReturn(method), + ValidateReturn: capabilityValidateAnyReturn(method), }, } } @@ -73,7 +73,7 @@ func (c *eventsCapability) callPublish(exec *Execution, receiver Value, args []V if err != nil { return NewNil(), err } - return c.cloneMethodResult(c.name+".publish", result) + return cloneCapabilityMethodResult(c.name+".publish", result) } func (c *eventsCapability) validatePublishContractArgs(args []Value, kwargs map[string]Value, block Value) error { @@ -92,16 +92,3 @@ func (c *eventsCapability) validatePublishContractArgs(args []Value, kwargs map[ } return validateCapabilityKwargsDataOnly(method, kwargs) } - -func (c *eventsCapability) validateMethodReturn(method string) func(result Value) error { - return func(result Value) error { - return validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny) - } -} - -func (c *eventsCapability) cloneMethodResult(method string, result Value) (Value, error) { - if err := validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny); err != nil { - return NewNil(), err - } - return deepCloneValue(result), nil -} diff --git a/vibes/capability_jobqueue.go b/vibes/capability_jobqueue.go index 7f43dce..eb79e9c 100644 --- a/vibes/capability_jobqueue.go +++ b/vibes/capability_jobqueue.go @@ -3,7 +3,6 @@ package vibes import ( "context" "fmt" - "maps" "time" ) @@ -68,279 +67,3 @@ type jobQueueCapability struct { queue JobQueue retry JobQueueWithRetry } - -func (c *jobQueueCapability) CapabilityContracts() map[string]CapabilityMethodContract { - contracts := map[string]CapabilityMethodContract{ - c.name + ".enqueue": { - ValidateArgs: c.validateEnqueueContractArgs, - ValidateReturn: c.validateMethodReturn(c.name + ".enqueue"), - }, - } - if c.retry != nil { - contracts[c.name+".retry"] = CapabilityMethodContract{ - ValidateArgs: c.validateRetryContractArgs, - ValidateReturn: c.validateMethodReturn(c.name + ".retry"), - } - } - return contracts -} - -func (c *jobQueueCapability) Bind(binding CapabilityBinding) (map[string]Value, error) { - methods := map[string]Value{ - "enqueue": NewBuiltin(c.name+".enqueue", c.callEnqueue), - } - if c.retry != nil { - methods["retry"] = NewBuiltin(c.name+".retry", c.callRetry) - } - return map[string]Value{c.name: NewObject(methods)}, nil -} - -func (c *jobQueueCapability) callEnqueue(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if len(args) != 2 { - return NewNil(), fmt.Errorf("%s.enqueue expects job name and payload", c.name) - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("%s.enqueue does not accept blocks", c.name) - } - - jobNameVal := args[0] - switch jobNameVal.Kind() { - case KindString, KindSymbol: - // supported - default: - return NewNil(), fmt.Errorf("%s.enqueue expects job name as string or symbol", c.name) - } - - payloadVal := args[1] - if payloadVal.Kind() != KindHash && payloadVal.Kind() != KindObject { - return NewNil(), fmt.Errorf("%s.enqueue expects payload hash", c.name) - } - - options, err := parseJobQueueEnqueueOptions(c.name, kwargs) - if err != nil { - return NewNil(), err - } - - job := JobQueueJob{ - Name: jobNameVal.String(), - Payload: cloneHash(payloadVal.Hash()), - Options: options, - } - - result, err := c.queue.Enqueue(exec.ctx, job) - if err != nil { - return NewNil(), err - } - return c.cloneMethodResult(c.name+".enqueue", result) -} - -func (c *jobQueueCapability) callRetry(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { - if c.retry == nil { - return NewNil(), fmt.Errorf("%s.retry is not supported", c.name) - } - if len(args) < 1 || len(args) > 2 { - return NewNil(), fmt.Errorf("%s.retry expects job id and optional options hash", c.name) - } - if !block.IsNil() { - return NewNil(), fmt.Errorf("%s.retry does not accept blocks", c.name) - } - - idVal := args[0] - if idVal.Kind() != KindString { - return NewNil(), fmt.Errorf("%s.retry expects job id string", c.name) - } - - options := make(map[string]Value) - if len(args) > 1 { - optsVal := args[1] - if optsVal.Kind() != KindHash && optsVal.Kind() != KindObject { - return NewNil(), fmt.Errorf("%s.retry options must be hash", c.name) - } - options = mergeHash(options, cloneHash(optsVal.Hash())) - } - options = mergeHash(options, cloneCapabilityKwargs(kwargs)) - - req := JobQueueRetryRequest{JobID: idVal.String(), Options: options} - result, err := c.retry.Retry(exec.ctx, req) - if err != nil { - return NewNil(), err - } - return c.cloneMethodResult(c.name+".retry", result) -} - -func (c *jobQueueCapability) validateEnqueueContractArgs(args []Value, kwargs map[string]Value, block Value) error { - method := c.name + ".enqueue" - - if len(args) != 2 { - return fmt.Errorf("%s expects job name and payload", method) - } - if !block.IsNil() { - return fmt.Errorf("%s does not accept blocks", method) - } - - jobNameVal := args[0] - switch jobNameVal.Kind() { - case KindString, KindSymbol: - // supported - default: - return fmt.Errorf("%s expects job name as string or symbol", method) - } - - payloadVal := args[1] - if err := validateCapabilityHashValue(method+" payload", payloadVal); err != nil { - return err - } - - return validateCapabilityKwargsDataOnly(method, kwargs) -} - -func (c *jobQueueCapability) validateRetryContractArgs(args []Value, kwargs map[string]Value, block Value) error { - method := c.name + ".retry" - - if len(args) < 1 || len(args) > 2 { - return fmt.Errorf("%s expects job id and optional options hash", method) - } - if !block.IsNil() { - return fmt.Errorf("%s does not accept blocks", method) - } - - idVal := args[0] - if idVal.Kind() != KindString { - return fmt.Errorf("%s expects job id string", method) - } - - if len(args) == 2 { - optionsVal := args[1] - if err := validateCapabilityHashValue(method+" options", optionsVal); err != nil { - return err - } - } - - return validateCapabilityKwargsDataOnly(method, kwargs) -} - -func (c *jobQueueCapability) validateMethodReturn(method string) func(result Value) error { - return func(result Value) error { - return validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny) - } -} - -func (c *jobQueueCapability) cloneMethodResult(method string, result Value) (Value, error) { - if err := validateCapabilityTypedValue(method+" return value", result, capabilityTypeAny); err != nil { - return NewNil(), err - } - return deepCloneValue(result), nil -} - -func parseJobQueueEnqueueOptions(name string, kwargs map[string]Value) (JobQueueEnqueueOptions, error) { - if len(kwargs) == 0 { - return JobQueueEnqueueOptions{}, nil - } - - var delay *time.Duration - var key *string - extra := make(map[string]Value) - - for k, v := range kwargs { - switch k { - case "delay": - d, err := valueToTimeDuration(name, v) - if err != nil { - return JobQueueEnqueueOptions{}, err - } - if d < 0 { - return JobQueueEnqueueOptions{}, fmt.Errorf("%s.enqueue delay must be non-negative", name) - } - delay = &d - case "key": - if v.Kind() != KindString { - return JobQueueEnqueueOptions{}, fmt.Errorf("%s.enqueue key must be a string", name) - } - s := v.String() - if s == "" { - return JobQueueEnqueueOptions{}, fmt.Errorf("%s.enqueue key must be non-empty", name) - } - key = &s - default: - extra[k] = deepCloneValue(v) - } - } - - opts := JobQueueEnqueueOptions{} - opts.Delay = delay - opts.Key = key - if len(extra) > 0 { - opts.Kwargs = extra - } - return opts, nil -} - -func valueToTimeDuration(name string, val Value) (time.Duration, error) { - switch val.Kind() { - case KindDuration: - secs := val.Duration().Seconds() - return time.Duration(secs) * time.Second, nil - case KindInt, KindFloat: - secs, err := valueToInt64(val) - if err != nil { - return 0, err - } - return time.Duration(secs) * time.Second, nil - default: - return 0, fmt.Errorf("%s.enqueue delay must be duration or numeric seconds", name) - } -} - -// cloneHash creates a deep copy of a hash to prevent mutations from affecting the original. -// For Values that contain references (arrays, hashes, objects), this recursively clones them. -func cloneHash(src map[string]Value) map[string]Value { - if len(src) == 0 { - return map[string]Value{} - } - out := make(map[string]Value, len(src)) - for k, v := range src { - out[k] = deepCloneValue(v) - } - return out -} - -// deepCloneValue recursively clones a Value and its contents. -func deepCloneValue(val Value) Value { - switch val.Kind() { - case KindArray: - arr := val.Array() - cloned := make([]Value, len(arr)) - for i, elem := range arr { - cloned[i] = deepCloneValue(elem) - } - return NewArray(cloned) - case KindHash: - hash := val.Hash() - cloned := make(map[string]Value, len(hash)) - for k, v := range hash { - cloned[k] = deepCloneValue(v) - } - return NewHash(cloned) - case KindObject: - obj := val.Hash() - cloned := make(map[string]Value, len(obj)) - for k, v := range obj { - cloned[k] = deepCloneValue(v) - } - return NewObject(cloned) - default: - // Primitive types (string, int, float, bool, nil, symbol, duration, money) are immutable or value types - return val - } -} - -func mergeHash(dest map[string]Value, src map[string]Value) map[string]Value { - if len(src) == 0 { - return dest - } - if dest == nil { - dest = make(map[string]Value, len(src)) - } - maps.Copy(dest, src) - return dest -} diff --git a/vibes/capability_jobqueue_calls.go b/vibes/capability_jobqueue_calls.go new file mode 100644 index 0000000..11641a4 --- /dev/null +++ b/vibes/capability_jobqueue_calls.go @@ -0,0 +1,86 @@ +package vibes + +import "fmt" + +func (c *jobQueueCapability) Bind(binding CapabilityBinding) (map[string]Value, error) { + methods := map[string]Value{ + "enqueue": NewBuiltin(c.name+".enqueue", c.callEnqueue), + } + if c.retry != nil { + methods["retry"] = NewBuiltin(c.name+".retry", c.callRetry) + } + return map[string]Value{c.name: NewObject(methods)}, nil +} + +func (c *jobQueueCapability) callEnqueue(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if len(args) != 2 { + return NewNil(), fmt.Errorf("%s.enqueue expects job name and payload", c.name) + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("%s.enqueue does not accept blocks", c.name) + } + + jobNameVal := args[0] + switch jobNameVal.Kind() { + case KindString, KindSymbol: + // supported + default: + return NewNil(), fmt.Errorf("%s.enqueue expects job name as string or symbol", c.name) + } + + payloadVal := args[1] + if payloadVal.Kind() != KindHash && payloadVal.Kind() != KindObject { + return NewNil(), fmt.Errorf("%s.enqueue expects payload hash", c.name) + } + + options, err := parseJobQueueEnqueueOptions(c.name, kwargs) + if err != nil { + return NewNil(), err + } + + job := JobQueueJob{ + Name: jobNameVal.String(), + Payload: cloneHash(payloadVal.Hash()), + Options: options, + } + + result, err := c.queue.Enqueue(exec.ctx, job) + if err != nil { + return NewNil(), err + } + return cloneCapabilityMethodResult(c.name+".enqueue", result) +} + +func (c *jobQueueCapability) callRetry(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) { + if c.retry == nil { + return NewNil(), fmt.Errorf("%s.retry is not supported", c.name) + } + if len(args) < 1 || len(args) > 2 { + return NewNil(), fmt.Errorf("%s.retry expects job id and optional options hash", c.name) + } + if !block.IsNil() { + return NewNil(), fmt.Errorf("%s.retry does not accept blocks", c.name) + } + + idVal := args[0] + if idVal.Kind() != KindString { + return NewNil(), fmt.Errorf("%s.retry expects job id string", c.name) + } + + options := make(map[string]Value) + if len(args) > 1 { + optsVal := args[1] + if optsVal.Kind() != KindHash && optsVal.Kind() != KindObject { + return NewNil(), fmt.Errorf("%s.retry options must be hash", c.name) + } + options = mergeHash(options, cloneHash(optsVal.Hash())) + } + options = mergeHash(options, cloneCapabilityKwargs(kwargs)) + + req := JobQueueRetryRequest{JobID: idVal.String(), Options: options} + result, err := c.retry.Retry(exec.ctx, req) + if err != nil { + return NewNil(), err + } + return cloneCapabilityMethodResult(c.name+".retry", result) +} diff --git a/vibes/capability_jobqueue_contracts.go b/vibes/capability_jobqueue_contracts.go new file mode 100644 index 0000000..2036fa4 --- /dev/null +++ b/vibes/capability_jobqueue_contracts.go @@ -0,0 +1,70 @@ +package vibes + +import "fmt" + +func (c *jobQueueCapability) CapabilityContracts() map[string]CapabilityMethodContract { + contracts := map[string]CapabilityMethodContract{ + c.name + ".enqueue": { + ValidateArgs: c.validateEnqueueContractArgs, + ValidateReturn: capabilityValidateAnyReturn(c.name + ".enqueue"), + }, + } + if c.retry != nil { + contracts[c.name+".retry"] = CapabilityMethodContract{ + ValidateArgs: c.validateRetryContractArgs, + ValidateReturn: capabilityValidateAnyReturn(c.name + ".retry"), + } + } + return contracts +} + +func (c *jobQueueCapability) validateEnqueueContractArgs(args []Value, kwargs map[string]Value, block Value) error { + method := c.name + ".enqueue" + + if len(args) != 2 { + return fmt.Errorf("%s expects job name and payload", method) + } + if !block.IsNil() { + return fmt.Errorf("%s does not accept blocks", method) + } + + jobNameVal := args[0] + switch jobNameVal.Kind() { + case KindString, KindSymbol: + // supported + default: + return fmt.Errorf("%s expects job name as string or symbol", method) + } + + payloadVal := args[1] + if err := validateCapabilityHashValue(method+" payload", payloadVal); err != nil { + return err + } + + return validateCapabilityKwargsDataOnly(method, kwargs) +} + +func (c *jobQueueCapability) validateRetryContractArgs(args []Value, kwargs map[string]Value, block Value) error { + method := c.name + ".retry" + + if len(args) < 1 || len(args) > 2 { + return fmt.Errorf("%s expects job id and optional options hash", method) + } + if !block.IsNil() { + return fmt.Errorf("%s does not accept blocks", method) + } + + idVal := args[0] + if idVal.Kind() != KindString { + return fmt.Errorf("%s expects job id string", method) + } + + if len(args) == 2 { + optionsVal := args[1] + if err := validateCapabilityHashValue(method+" options", optionsVal); err != nil { + return err + } + } + + return validateCapabilityKwargsDataOnly(method, kwargs) +} diff --git a/vibes/capability_jobqueue_options.go b/vibes/capability_jobqueue_options.go new file mode 100644 index 0000000..42b513d --- /dev/null +++ b/vibes/capability_jobqueue_options.go @@ -0,0 +1,65 @@ +package vibes + +import ( + "fmt" + "time" +) + +func parseJobQueueEnqueueOptions(name string, kwargs map[string]Value) (JobQueueEnqueueOptions, error) { + if len(kwargs) == 0 { + return JobQueueEnqueueOptions{}, nil + } + + var delay *time.Duration + var key *string + extra := make(map[string]Value) + + for k, v := range kwargs { + switch k { + case "delay": + d, err := valueToTimeDuration(name, v) + if err != nil { + return JobQueueEnqueueOptions{}, err + } + if d < 0 { + return JobQueueEnqueueOptions{}, fmt.Errorf("%s.enqueue delay must be non-negative", name) + } + delay = &d + case "key": + if v.Kind() != KindString { + return JobQueueEnqueueOptions{}, fmt.Errorf("%s.enqueue key must be a string", name) + } + s := v.String() + if s == "" { + return JobQueueEnqueueOptions{}, fmt.Errorf("%s.enqueue key must be non-empty", name) + } + key = &s + default: + extra[k] = deepCloneValue(v) + } + } + + opts := JobQueueEnqueueOptions{} + opts.Delay = delay + opts.Key = key + if len(extra) > 0 { + opts.Kwargs = extra + } + return opts, nil +} + +func valueToTimeDuration(name string, val Value) (time.Duration, error) { + switch val.Kind() { + case KindDuration: + secs := val.Duration().Seconds() + return time.Duration(secs) * time.Second, nil + case KindInt, KindFloat: + secs, err := valueToInt64(val) + if err != nil { + return 0, err + } + return time.Duration(secs) * time.Second, nil + default: + return 0, fmt.Errorf("%s.enqueue delay must be duration or numeric seconds", name) + } +} diff --git a/vibes/execution.go b/vibes/execution.go index 3f966a7..af40f4c 100644 --- a/vibes/execution.go +++ b/vibes/execution.go @@ -2,17 +2,6 @@ package vibes import ( "context" - "errors" - "fmt" - "maps" - "math" - "reflect" - "regexp" - "slices" - "sort" - "strings" - "time" - "unicode" ) type ScriptFunction struct { @@ -84,4942 +73,3 @@ type callFrame struct { Function string 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: "