@rcrsr/rill 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -8
- package/dist/check/config.d.ts +20 -0
- package/dist/check/config.d.ts.map +1 -0
- package/dist/check/config.js +151 -0
- package/dist/check/config.js.map +1 -0
- package/dist/check/fixer.d.ts +39 -0
- package/dist/check/fixer.d.ts.map +1 -0
- package/dist/check/fixer.js +119 -0
- package/dist/check/fixer.js.map +1 -0
- package/dist/check/index.d.ts +10 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +21 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/rules/anti-patterns.d.ts +65 -0
- package/dist/check/rules/anti-patterns.d.ts.map +1 -0
- package/dist/check/rules/anti-patterns.js +427 -0
- package/dist/check/rules/anti-patterns.js.map +1 -0
- package/dist/check/rules/closures.d.ts +66 -0
- package/dist/check/rules/closures.d.ts.map +1 -0
- package/dist/check/rules/closures.js +373 -0
- package/dist/check/rules/closures.js.map +1 -0
- package/dist/check/rules/collections.d.ts +90 -0
- package/dist/check/rules/collections.d.ts.map +1 -0
- package/dist/check/rules/collections.js +373 -0
- package/dist/check/rules/collections.js.map +1 -0
- package/dist/check/rules/conditionals.d.ts +41 -0
- package/dist/check/rules/conditionals.d.ts.map +1 -0
- package/dist/check/rules/conditionals.js +106 -0
- package/dist/check/rules/conditionals.js.map +1 -0
- package/dist/check/rules/flow.d.ts +46 -0
- package/dist/check/rules/flow.d.ts.map +1 -0
- package/dist/check/rules/flow.js +206 -0
- package/dist/check/rules/flow.js.map +1 -0
- package/dist/check/rules/formatting.d.ts +133 -0
- package/dist/check/rules/formatting.d.ts.map +1 -0
- package/dist/check/rules/formatting.js +639 -0
- package/dist/check/rules/formatting.js.map +1 -0
- package/dist/check/rules/helpers.d.ts +26 -0
- package/dist/check/rules/helpers.d.ts.map +1 -0
- package/dist/check/rules/helpers.js +66 -0
- package/dist/check/rules/helpers.js.map +1 -0
- package/dist/check/rules/index.d.ts +21 -0
- package/dist/check/rules/index.d.ts.map +1 -0
- package/dist/check/rules/index.js +78 -0
- package/dist/check/rules/index.js.map +1 -0
- package/dist/check/rules/loops.d.ts +70 -0
- package/dist/check/rules/loops.d.ts.map +1 -0
- package/dist/check/rules/loops.js +227 -0
- package/dist/check/rules/loops.js.map +1 -0
- package/dist/check/rules/naming.d.ts +21 -0
- package/dist/check/rules/naming.d.ts.map +1 -0
- package/dist/check/rules/naming.js +167 -0
- package/dist/check/rules/naming.js.map +1 -0
- package/dist/check/rules/strings.d.ts +28 -0
- package/dist/check/rules/strings.d.ts.map +1 -0
- package/dist/check/rules/strings.js +80 -0
- package/dist/check/rules/strings.js.map +1 -0
- package/dist/check/rules/types.d.ts +41 -0
- package/dist/check/rules/types.d.ts.map +1 -0
- package/dist/check/rules/types.js +162 -0
- package/dist/check/rules/types.js.map +1 -0
- package/dist/check/types.d.ts +106 -0
- package/dist/check/types.d.ts.map +1 -0
- package/dist/check/types.js +6 -0
- package/dist/check/types.js.map +1 -0
- package/dist/check/validator.d.ts +18 -0
- package/dist/check/validator.d.ts.map +1 -0
- package/dist/check/validator.js +88 -0
- package/dist/check/validator.js.map +1 -0
- package/dist/check/visitor.d.ts +33 -0
- package/dist/check/visitor.d.ts.map +1 -0
- package/dist/check/visitor.js +243 -0
- package/dist/check/visitor.js.map +1 -0
- package/dist/cli-check.d.ts +43 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli-check.js +356 -0
- package/dist/cli-check.js.map +1 -0
- package/dist/cli-eval.d.ts +15 -0
- package/dist/cli-eval.d.ts.map +1 -0
- package/dist/cli-eval.js +120 -0
- package/dist/cli-eval.js.map +1 -0
- package/dist/cli-exec.d.ts +49 -0
- package/dist/cli-exec.d.ts.map +1 -0
- package/dist/cli-exec.js +191 -0
- package/dist/cli-exec.js.map +1 -0
- package/dist/cli-module-loader.d.ts +19 -0
- package/dist/cli-module-loader.d.ts.map +1 -0
- package/dist/cli-module-loader.js +83 -0
- package/dist/cli-module-loader.js.map +1 -0
- package/dist/cli-shared.d.ts +36 -0
- package/dist/cli-shared.d.ts.map +1 -0
- package/dist/cli-shared.js +101 -0
- package/dist/cli-shared.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer/readers.d.ts +1 -1
- package/dist/lexer/readers.d.ts.map +1 -1
- package/dist/lexer/readers.js +62 -32
- package/dist/lexer/readers.js.map +1 -1
- package/dist/lexer/tokenizer.d.ts.map +1 -1
- package/dist/lexer/tokenizer.js +5 -6
- package/dist/lexer/tokenizer.js.map +1 -1
- package/dist/parser/index.js +1 -1
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/parser-expr.js +23 -5
- package/dist/parser/parser-expr.js.map +1 -1
- package/dist/parser/parser-functions.d.ts +2 -2
- package/dist/parser/parser-functions.d.ts.map +1 -1
- package/dist/parser/parser-functions.js +2 -1
- package/dist/parser/parser-functions.js.map +1 -1
- package/dist/parser/parser-literals.js +2 -2
- package/dist/parser/parser-literals.js.map +1 -1
- package/dist/parser/parser-script.js +9 -7
- package/dist/parser/parser-script.js.map +1 -1
- package/dist/parser/parser-variables.js +4 -3
- package/dist/parser/parser-variables.js.map +1 -1
- package/dist/runtime/core/callable.d.ts +5 -6
- package/dist/runtime/core/callable.d.ts.map +1 -1
- package/dist/runtime/core/callable.js.map +1 -1
- package/dist/runtime/core/context.d.ts.map +1 -1
- package/dist/runtime/core/context.js +19 -32
- package/dist/runtime/core/context.js.map +1 -1
- package/dist/runtime/core/equals.js +1 -1
- package/dist/runtime/core/equals.js.map +1 -1
- package/dist/runtime/core/eval/evaluator.d.ts +78 -0
- package/dist/runtime/core/eval/evaluator.d.ts.map +1 -1
- package/dist/runtime/core/eval/evaluator.js +78 -0
- package/dist/runtime/core/eval/evaluator.js.map +1 -1
- package/dist/runtime/core/eval/mixins/closures.d.ts.map +1 -1
- package/dist/runtime/core/eval/mixins/closures.js +9 -1
- package/dist/runtime/core/eval/mixins/closures.js.map +1 -1
- package/dist/runtime/core/eval/mixins/variables.d.ts.map +1 -1
- package/dist/runtime/core/eval/mixins/variables.js +143 -2
- package/dist/runtime/core/eval/mixins/variables.js.map +1 -1
- package/dist/runtime/core/types.d.ts +15 -2
- package/dist/runtime/core/types.d.ts.map +1 -1
- package/dist/runtime/core/types.js.map +1 -1
- package/dist/runtime/ext/extensions.d.ts +51 -0
- package/dist/runtime/ext/extensions.d.ts.map +1 -0
- package/dist/runtime/ext/extensions.js +67 -0
- package/dist/runtime/ext/extensions.js.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -4
- package/dist/types.js.map +1 -1
- package/docs/00_INDEX.md +1 -0
- package/docs/01_guide.md +3 -3
- package/docs/02_types.md +8 -10
- package/docs/03_variables.md +10 -0
- package/docs/04_operators.md +3 -3
- package/docs/05_control-flow.md +21 -0
- package/docs/07_collections.md +2 -0
- package/docs/10_parsing.md +9 -9
- package/docs/11_reference.md +1 -1
- package/docs/12_examples.md +36 -62
- package/docs/14_host-integration.md +116 -111
- package/docs/15_grammar.ebnf +1 -5
- package/docs/16_conventions.md +3 -4
- package/docs/17_cli-tools.md +184 -0
- package/docs/99_llm-reference.txt +46 -5
- package/package.json +13 -4
- package/dist/demo.d.ts +0 -6
- package/dist/demo.d.ts.map +0 -1
- package/dist/demo.js +0 -121
- package/dist/demo.js.map +0 -1
- package/dist/lexer.d.ts +0 -19
- package/dist/lexer.d.ts.map +0 -1
- package/dist/lexer.js +0 -344
- package/dist/lexer.js.map +0 -1
- package/dist/parser/arithmetic.d.ts +0 -16
- package/dist/parser/arithmetic.d.ts.map +0 -1
- package/dist/parser/arithmetic.js +0 -128
- package/dist/parser/arithmetic.js.map +0 -1
- package/dist/parser/boolean.d.ts +0 -15
- package/dist/parser/boolean.d.ts.map +0 -1
- package/dist/parser/boolean.js +0 -20
- package/dist/parser/boolean.js.map +0 -1
- package/dist/parser/control-flow.d.ts +0 -56
- package/dist/parser/control-flow.d.ts.map +0 -1
- package/dist/parser/control-flow.js +0 -167
- package/dist/parser/control-flow.js.map +0 -1
- package/dist/parser/expressions.d.ts +0 -23
- package/dist/parser/expressions.d.ts.map +0 -1
- package/dist/parser/expressions.js +0 -950
- package/dist/parser/expressions.js.map +0 -1
- package/dist/parser/extraction.d.ts +0 -48
- package/dist/parser/extraction.d.ts.map +0 -1
- package/dist/parser/extraction.js +0 -279
- package/dist/parser/extraction.js.map +0 -1
- package/dist/parser/functions.d.ts +0 -20
- package/dist/parser/functions.d.ts.map +0 -1
- package/dist/parser/functions.js +0 -96
- package/dist/parser/functions.js.map +0 -1
- package/dist/parser/literals.d.ts +0 -37
- package/dist/parser/literals.d.ts.map +0 -1
- package/dist/parser/literals.js +0 -373
- package/dist/parser/literals.js.map +0 -1
- package/dist/parser/script.d.ts +0 -14
- package/dist/parser/script.d.ts.map +0 -1
- package/dist/parser/script.js +0 -196
- package/dist/parser/script.js.map +0 -1
- package/dist/parser/variables.d.ts +0 -10
- package/dist/parser/variables.d.ts.map +0 -1
- package/dist/parser/variables.js +0 -215
- package/dist/parser/variables.js.map +0 -1
- package/dist/runtime/ast-equals.d.ts +0 -13
- package/dist/runtime/ast-equals.d.ts.map +0 -1
- package/dist/runtime/ast-equals.js +0 -447
- package/dist/runtime/ast-equals.js.map +0 -1
- package/dist/runtime/builtins.d.ts +0 -13
- package/dist/runtime/builtins.d.ts.map +0 -1
- package/dist/runtime/builtins.js +0 -180
- package/dist/runtime/builtins.js.map +0 -1
- package/dist/runtime/callable.d.ts +0 -88
- package/dist/runtime/callable.d.ts.map +0 -1
- package/dist/runtime/callable.js +0 -98
- package/dist/runtime/callable.js.map +0 -1
- package/dist/runtime/context.d.ts +0 -13
- package/dist/runtime/context.d.ts.map +0 -1
- package/dist/runtime/context.js +0 -73
- package/dist/runtime/context.js.map +0 -1
- package/dist/runtime/core/evaluate.d.ts +0 -42
- package/dist/runtime/core/evaluate.d.ts.map +0 -1
- package/dist/runtime/core/evaluate.debug.js +0 -1251
- package/dist/runtime/core/evaluate.js +0 -1913
- package/dist/runtime/core/evaluate.js.map +0 -1
- package/dist/runtime/evaluate.d.ts +0 -32
- package/dist/runtime/evaluate.d.ts.map +0 -1
- package/dist/runtime/evaluate.js +0 -1111
- package/dist/runtime/evaluate.js.map +0 -1
- package/dist/runtime/execute.d.ts +0 -26
- package/dist/runtime/execute.d.ts.map +0 -1
- package/dist/runtime/execute.js +0 -121
- package/dist/runtime/execute.js.map +0 -1
- package/dist/runtime/signals.d.ts +0 -19
- package/dist/runtime/signals.d.ts.map +0 -1
- package/dist/runtime/signals.js +0 -26
- package/dist/runtime/signals.js.map +0 -1
- package/dist/runtime/types.d.ts +0 -169
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -50
- package/dist/runtime/types.js.map +0 -1
- package/dist/runtime/values.d.ts +0 -50
- package/dist/runtime/values.d.ts.map +0 -1
- package/dist/runtime/values.js +0 -209
- package/dist/runtime/values.js.map +0 -1
- package/dist/runtime.d.ts +0 -254
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -2014
- package/dist/runtime.js.map +0 -1
package/dist/runtime.js
DELETED
|
@@ -1,2014 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rill Runtime
|
|
3
|
-
* Executes parsed Rill AST with pluggable context and I/O
|
|
4
|
-
*/
|
|
5
|
-
import { AbortError, AutoExceptionError, RILL_ERROR_CODES, RuntimeError, TimeoutError, } from './types.js';
|
|
6
|
-
// Re-export error classes for backwards compatibility
|
|
7
|
-
export { AbortError, AutoExceptionError, RuntimeError, TimeoutError, } from './types.js';
|
|
8
|
-
/** Type guard for any callable */
|
|
9
|
-
export function isCallable(value) {
|
|
10
|
-
return (typeof value === 'object' &&
|
|
11
|
-
value !== null &&
|
|
12
|
-
'__type' in value &&
|
|
13
|
-
value.__type === 'callable');
|
|
14
|
-
}
|
|
15
|
-
/** Type guard for script callable */
|
|
16
|
-
export function isScriptCallable(value) {
|
|
17
|
-
return isCallable(value) && value.kind === 'script';
|
|
18
|
-
}
|
|
19
|
-
/** Type guard for runtime callable */
|
|
20
|
-
export function isRuntimeCallable(value) {
|
|
21
|
-
return isCallable(value) && value.kind === 'runtime';
|
|
22
|
-
}
|
|
23
|
-
/** Type guard for application callable */
|
|
24
|
-
export function isApplicationCallable(value) {
|
|
25
|
-
return isCallable(value) && value.kind === 'application';
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Create an application callable from a host function.
|
|
29
|
-
* @param fn The function to wrap
|
|
30
|
-
* @param isProperty If true, auto-invokes when accessed from dict (property-style)
|
|
31
|
-
*/
|
|
32
|
-
export function callable(fn, isProperty = false) {
|
|
33
|
-
return { __type: 'callable', kind: 'application', fn, isProperty };
|
|
34
|
-
}
|
|
35
|
-
/** Type guard for dict (plain object, not array, not callable, not args) */
|
|
36
|
-
export function isDict(value) {
|
|
37
|
-
return (typeof value === 'object' &&
|
|
38
|
-
value !== null &&
|
|
39
|
-
!Array.isArray(value) &&
|
|
40
|
-
!isCallable(value) &&
|
|
41
|
-
!isArgs(value));
|
|
42
|
-
}
|
|
43
|
-
/** Type guard for RillArgs */
|
|
44
|
-
export function isArgs(value) {
|
|
45
|
-
return (typeof value === 'object' &&
|
|
46
|
-
value !== null &&
|
|
47
|
-
'__rill_args' in value &&
|
|
48
|
-
value.__rill_args === true);
|
|
49
|
-
}
|
|
50
|
-
/** Create args from a tuple (positional) */
|
|
51
|
-
function createArgsFromTuple(tuple) {
|
|
52
|
-
const entries = new Map();
|
|
53
|
-
for (let i = 0; i < tuple.length; i++) {
|
|
54
|
-
const val = tuple[i];
|
|
55
|
-
if (val !== undefined) {
|
|
56
|
-
entries.set(i, val);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return { __rill_args: true, entries };
|
|
60
|
-
}
|
|
61
|
-
/** Create args from a dict (named) */
|
|
62
|
-
function createArgsFromDict(dict) {
|
|
63
|
-
const entries = new Map();
|
|
64
|
-
for (const [key, value] of Object.entries(dict)) {
|
|
65
|
-
entries.set(key, value);
|
|
66
|
-
}
|
|
67
|
-
return { __rill_args: true, entries };
|
|
68
|
-
}
|
|
69
|
-
/** Reserved dict method names that cannot be overridden */
|
|
70
|
-
export const RESERVED_DICT_METHODS = ['keys', 'values', 'entries'];
|
|
71
|
-
/** Check if a key name is reserved */
|
|
72
|
-
export function isReservedMethod(name) {
|
|
73
|
-
return RESERVED_DICT_METHODS.includes(name);
|
|
74
|
-
}
|
|
75
|
-
// ============================================================
|
|
76
|
-
// RUNTIME ERROR HELPERS
|
|
77
|
-
// ============================================================
|
|
78
|
-
/** Helper to get location from an AST node */
|
|
79
|
-
function getNodeLocation(node) {
|
|
80
|
-
return node?.span.start;
|
|
81
|
-
}
|
|
82
|
-
// ============================================================
|
|
83
|
-
// CONTROL FLOW SIGNALS
|
|
84
|
-
// ============================================================
|
|
85
|
-
/** Signal thrown by `break` to exit loops */
|
|
86
|
-
export class BreakSignal extends Error {
|
|
87
|
-
value;
|
|
88
|
-
constructor(value) {
|
|
89
|
-
super('break');
|
|
90
|
-
this.value = value;
|
|
91
|
-
this.name = 'BreakSignal';
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/** Signal thrown by `return` to exit blocks */
|
|
95
|
-
export class ReturnSignal extends Error {
|
|
96
|
-
value;
|
|
97
|
-
constructor(value) {
|
|
98
|
-
super('return');
|
|
99
|
-
this.value = value;
|
|
100
|
-
this.name = 'ReturnSignal';
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// ============================================================
|
|
104
|
-
// CONTEXT FACTORY
|
|
105
|
-
// ============================================================
|
|
106
|
-
const defaultCallbacks = {
|
|
107
|
-
onLog: (value) => {
|
|
108
|
-
console.log(formatValue(value));
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
/** Infer the Rill type from a runtime value */
|
|
112
|
-
function inferType(value) {
|
|
113
|
-
if (value === null)
|
|
114
|
-
return 'string'; // null treated as empty string
|
|
115
|
-
if (typeof value === 'string')
|
|
116
|
-
return 'string';
|
|
117
|
-
if (typeof value === 'number')
|
|
118
|
-
return 'number';
|
|
119
|
-
if (typeof value === 'boolean')
|
|
120
|
-
return 'bool';
|
|
121
|
-
if (isScriptCallable(value))
|
|
122
|
-
return 'closure';
|
|
123
|
-
if (isArgs(value))
|
|
124
|
-
return 'args';
|
|
125
|
-
if (Array.isArray(value))
|
|
126
|
-
return 'tuple';
|
|
127
|
-
if (typeof value === 'object')
|
|
128
|
-
return 'dict';
|
|
129
|
-
return 'string'; // fallback
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Set a variable with type checking.
|
|
133
|
-
* - First assignment locks the type (inferred or explicit)
|
|
134
|
-
* - Subsequent assignments must match the locked type
|
|
135
|
-
* - Explicit type annotation is validated against value type
|
|
136
|
-
*/
|
|
137
|
-
function setVariable(ctx, name, value, explicitType, location) {
|
|
138
|
-
const valueType = inferType(value);
|
|
139
|
-
// Check explicit type annotation matches value
|
|
140
|
-
if (explicitType !== null && explicitType !== valueType) {
|
|
141
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Type mismatch: cannot assign ${valueType} to $${name}:${explicitType}`, location, { variableName: name, expectedType: explicitType, actualType: valueType });
|
|
142
|
-
}
|
|
143
|
-
// Check if variable already has a locked type
|
|
144
|
-
const lockedType = ctx.variableTypes.get(name);
|
|
145
|
-
if (lockedType !== undefined && lockedType !== valueType) {
|
|
146
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Type mismatch: cannot assign ${valueType} to $${name} (locked as ${lockedType})`, location, { variableName: name, expectedType: lockedType, actualType: valueType });
|
|
147
|
-
}
|
|
148
|
-
// Set the variable and lock its type
|
|
149
|
-
ctx.variables.set(name, value);
|
|
150
|
-
if (!ctx.variableTypes.has(name)) {
|
|
151
|
-
ctx.variableTypes.set(name, explicitType ?? valueType);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Bind callables in a dict to their containing dict.
|
|
156
|
-
* This sets boundDict on each callable so they can access their container.
|
|
157
|
-
*/
|
|
158
|
-
function bindDictCallables(value) {
|
|
159
|
-
if (!isDict(value))
|
|
160
|
-
return value;
|
|
161
|
-
const dict = value;
|
|
162
|
-
let hasBoundCallables = false;
|
|
163
|
-
// Check if any values are callables that need binding
|
|
164
|
-
for (const v of Object.values(dict)) {
|
|
165
|
-
if (isCallable(v) && !v.boundDict) {
|
|
166
|
-
hasBoundCallables = true;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (!hasBoundCallables)
|
|
171
|
-
return value;
|
|
172
|
-
// Create a new dict with bound callables
|
|
173
|
-
const result = {};
|
|
174
|
-
for (const [key, v] of Object.entries(dict)) {
|
|
175
|
-
if (isCallable(v) && !v.boundDict) {
|
|
176
|
-
result[key] = { ...v, boundDict: result };
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
result[key] = v;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
export function createRuntimeContext(options = {}) {
|
|
185
|
-
const variables = new Map();
|
|
186
|
-
const variableTypes = new Map();
|
|
187
|
-
const functions = new Map();
|
|
188
|
-
const methods = new Map();
|
|
189
|
-
// Set initial variables (and infer their types)
|
|
190
|
-
if (options.variables) {
|
|
191
|
-
for (const [name, value] of Object.entries(options.variables)) {
|
|
192
|
-
// Bind callables in dicts to their containing dict
|
|
193
|
-
const boundValue = bindDictCallables(value);
|
|
194
|
-
variables.set(name, boundValue);
|
|
195
|
-
variableTypes.set(name, inferType(boundValue));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// Set built-in functions
|
|
199
|
-
for (const [name, fn] of Object.entries(BUILTIN_FUNCTIONS)) {
|
|
200
|
-
functions.set(name, fn);
|
|
201
|
-
}
|
|
202
|
-
// Set custom functions (can override built-ins)
|
|
203
|
-
if (options.functions) {
|
|
204
|
-
for (const [name, fn] of Object.entries(options.functions)) {
|
|
205
|
-
functions.set(name, fn);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Set built-in methods
|
|
209
|
-
for (const [name, impl] of Object.entries(BUILTIN_METHODS)) {
|
|
210
|
-
methods.set(name, impl);
|
|
211
|
-
}
|
|
212
|
-
// Compile autoException patterns into RegExp objects
|
|
213
|
-
const autoExceptions = [];
|
|
214
|
-
if (options.autoExceptions) {
|
|
215
|
-
for (const pattern of options.autoExceptions) {
|
|
216
|
-
try {
|
|
217
|
-
autoExceptions.push(new RegExp(pattern));
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_INVALID_PATTERN, `Invalid autoException pattern: ${pattern}`, undefined, { pattern });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return {
|
|
225
|
-
variables,
|
|
226
|
-
variableTypes,
|
|
227
|
-
functions,
|
|
228
|
-
methods,
|
|
229
|
-
callbacks: { ...defaultCallbacks, ...options.callbacks },
|
|
230
|
-
observability: options.observability ?? {},
|
|
231
|
-
pipeValue: null,
|
|
232
|
-
timeout: options.timeout,
|
|
233
|
-
autoExceptions,
|
|
234
|
-
signal: options.signal,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
// ============================================================
|
|
238
|
-
// BUILT-IN FUNCTIONS
|
|
239
|
-
// ============================================================
|
|
240
|
-
const BUILTIN_FUNCTIONS = {
|
|
241
|
-
/** Identity function - returns its argument */
|
|
242
|
-
identity: (args) => args[0] ?? null,
|
|
243
|
-
/** Return the type name of a value */
|
|
244
|
-
type: (args) => inferType(args[0] ?? null),
|
|
245
|
-
/** Log a value and return it unchanged (passthrough) */
|
|
246
|
-
log: (args, ctx) => {
|
|
247
|
-
const value = args[0] ?? null;
|
|
248
|
-
ctx.callbacks.onLog(value);
|
|
249
|
-
return value;
|
|
250
|
-
},
|
|
251
|
-
/** Convert any value to JSON string */
|
|
252
|
-
json: (args) => JSON.stringify(args[0] ?? null),
|
|
253
|
-
};
|
|
254
|
-
// ============================================================
|
|
255
|
-
// BUILT-IN METHODS
|
|
256
|
-
// ============================================================
|
|
257
|
-
const BUILTIN_METHODS = {
|
|
258
|
-
// === Conversion methods ===
|
|
259
|
-
/** Convert value to string */
|
|
260
|
-
str: (receiver) => formatValue(receiver),
|
|
261
|
-
/** Convert value to number */
|
|
262
|
-
num: (receiver) => {
|
|
263
|
-
if (typeof receiver === 'number')
|
|
264
|
-
return receiver;
|
|
265
|
-
if (typeof receiver === 'string') {
|
|
266
|
-
const n = parseFloat(receiver);
|
|
267
|
-
if (!isNaN(n))
|
|
268
|
-
return n;
|
|
269
|
-
}
|
|
270
|
-
if (typeof receiver === 'boolean')
|
|
271
|
-
return receiver ? 1 : 0;
|
|
272
|
-
return 0;
|
|
273
|
-
},
|
|
274
|
-
/** Get length of string or array */
|
|
275
|
-
len: (receiver) => {
|
|
276
|
-
if (typeof receiver === 'string')
|
|
277
|
-
return receiver.length;
|
|
278
|
-
if (Array.isArray(receiver))
|
|
279
|
-
return receiver.length;
|
|
280
|
-
if (receiver && typeof receiver === 'object') {
|
|
281
|
-
return Object.keys(receiver).length;
|
|
282
|
-
}
|
|
283
|
-
return 0;
|
|
284
|
-
},
|
|
285
|
-
/** Trim whitespace from string */
|
|
286
|
-
trim: (receiver) => formatValue(receiver).trim(),
|
|
287
|
-
// === Element access methods ===
|
|
288
|
-
/** Get first element of array or first char of string */
|
|
289
|
-
first: (receiver) => {
|
|
290
|
-
if (Array.isArray(receiver))
|
|
291
|
-
return receiver[0] ?? null;
|
|
292
|
-
if (typeof receiver === 'string')
|
|
293
|
-
return receiver[0] ?? '';
|
|
294
|
-
return null;
|
|
295
|
-
},
|
|
296
|
-
/** Get last element of array or last char of string */
|
|
297
|
-
last: (receiver) => {
|
|
298
|
-
if (Array.isArray(receiver))
|
|
299
|
-
return receiver[receiver.length - 1] ?? null;
|
|
300
|
-
if (typeof receiver === 'string') {
|
|
301
|
-
return receiver[receiver.length - 1] ?? '';
|
|
302
|
-
}
|
|
303
|
-
return null;
|
|
304
|
-
},
|
|
305
|
-
/** Get element at index */
|
|
306
|
-
at: (receiver, args) => {
|
|
307
|
-
const idx = typeof args[0] === 'number' ? args[0] : 0;
|
|
308
|
-
if (Array.isArray(receiver))
|
|
309
|
-
return receiver[idx] ?? null;
|
|
310
|
-
if (typeof receiver === 'string')
|
|
311
|
-
return receiver[idx] ?? '';
|
|
312
|
-
return null;
|
|
313
|
-
},
|
|
314
|
-
// === String operations ===
|
|
315
|
-
/** Split string by separator (default: newline) */
|
|
316
|
-
split: (receiver, args) => {
|
|
317
|
-
const str = formatValue(receiver);
|
|
318
|
-
const sep = typeof args[0] === 'string' ? args[0] : '\n';
|
|
319
|
-
return str.split(sep);
|
|
320
|
-
},
|
|
321
|
-
/** Join array elements with separator (default: comma) */
|
|
322
|
-
join: (receiver, args) => {
|
|
323
|
-
const sep = typeof args[0] === 'string' ? args[0] : ',';
|
|
324
|
-
if (!Array.isArray(receiver))
|
|
325
|
-
return formatValue(receiver);
|
|
326
|
-
return receiver.map(formatValue).join(sep);
|
|
327
|
-
},
|
|
328
|
-
/** Split string into lines (same as .split but newline only) */
|
|
329
|
-
lines: (receiver) => {
|
|
330
|
-
const str = formatValue(receiver);
|
|
331
|
-
return str.split('\n');
|
|
332
|
-
},
|
|
333
|
-
// === Utility methods ===
|
|
334
|
-
/** Check if value is empty */
|
|
335
|
-
empty: (receiver) => isEmpty(receiver),
|
|
336
|
-
// === Pattern matching methods ===
|
|
337
|
-
/** Check if string contains substring */
|
|
338
|
-
contains: (receiver, args) => {
|
|
339
|
-
const str = formatValue(receiver);
|
|
340
|
-
const search = formatValue(args[0] ?? '');
|
|
341
|
-
return str.includes(search);
|
|
342
|
-
},
|
|
343
|
-
/** Match regex pattern and return capture groups as tuple. Empty tuple = no match. */
|
|
344
|
-
matches: (receiver, args) => {
|
|
345
|
-
const str = formatValue(receiver);
|
|
346
|
-
const pattern = formatValue(args[0] ?? '');
|
|
347
|
-
try {
|
|
348
|
-
const match = new RegExp(pattern).exec(str);
|
|
349
|
-
if (!match)
|
|
350
|
-
return [];
|
|
351
|
-
// Return capture groups (index 1+), or full match if no groups
|
|
352
|
-
const groups = match.slice(1);
|
|
353
|
-
return groups.length > 0 ? groups : [match[0]];
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
return [];
|
|
357
|
-
}
|
|
358
|
-
},
|
|
359
|
-
// === Comparison methods ===
|
|
360
|
-
/** Equality check (deep structural comparison) */
|
|
361
|
-
eq: (receiver, args) => deepEquals(receiver, args[0] ?? null),
|
|
362
|
-
/** Inequality check (deep structural comparison) */
|
|
363
|
-
ne: (receiver, args) => !deepEquals(receiver, args[0] ?? null),
|
|
364
|
-
/** Less than */
|
|
365
|
-
lt: (receiver, args) => {
|
|
366
|
-
if (typeof receiver === 'number' && typeof args[0] === 'number') {
|
|
367
|
-
return receiver < args[0];
|
|
368
|
-
}
|
|
369
|
-
return formatValue(receiver) < formatValue(args[0] ?? '');
|
|
370
|
-
},
|
|
371
|
-
/** Greater than */
|
|
372
|
-
gt: (receiver, args) => {
|
|
373
|
-
if (typeof receiver === 'number' && typeof args[0] === 'number') {
|
|
374
|
-
return receiver > args[0];
|
|
375
|
-
}
|
|
376
|
-
return formatValue(receiver) > formatValue(args[0] ?? '');
|
|
377
|
-
},
|
|
378
|
-
/** Less than or equal */
|
|
379
|
-
le: (receiver, args) => {
|
|
380
|
-
if (typeof receiver === 'number' && typeof args[0] === 'number') {
|
|
381
|
-
return receiver <= args[0];
|
|
382
|
-
}
|
|
383
|
-
return formatValue(receiver) <= formatValue(args[0] ?? '');
|
|
384
|
-
},
|
|
385
|
-
/** Greater than or equal */
|
|
386
|
-
ge: (receiver, args) => {
|
|
387
|
-
if (typeof receiver === 'number' && typeof args[0] === 'number') {
|
|
388
|
-
return receiver >= args[0];
|
|
389
|
-
}
|
|
390
|
-
return formatValue(receiver) >= formatValue(args[0] ?? '');
|
|
391
|
-
},
|
|
392
|
-
// === Dict methods (reserved) ===
|
|
393
|
-
/** Get all keys of a dict as a tuple of strings */
|
|
394
|
-
keys: (receiver) => {
|
|
395
|
-
if (isDict(receiver)) {
|
|
396
|
-
return Object.keys(receiver);
|
|
397
|
-
}
|
|
398
|
-
return [];
|
|
399
|
-
},
|
|
400
|
-
/** Get all values of a dict as a tuple */
|
|
401
|
-
values: (receiver) => {
|
|
402
|
-
if (isDict(receiver)) {
|
|
403
|
-
return Object.values(receiver);
|
|
404
|
-
}
|
|
405
|
-
return [];
|
|
406
|
-
},
|
|
407
|
-
/** Get all entries of a dict as a tuple of [key, value] pairs */
|
|
408
|
-
entries: (receiver) => {
|
|
409
|
-
if (isDict(receiver)) {
|
|
410
|
-
return Object.entries(receiver).map(([k, v]) => [k, v]);
|
|
411
|
-
}
|
|
412
|
-
return [];
|
|
413
|
-
},
|
|
414
|
-
};
|
|
415
|
-
// ============================================================
|
|
416
|
-
// AUTO-EXCEPTION CHECKING
|
|
417
|
-
// ============================================================
|
|
418
|
-
/**
|
|
419
|
-
* Check if the current pipe value matches any autoException pattern.
|
|
420
|
-
* Only checks string values. Throws AutoExceptionError on match.
|
|
421
|
-
*/
|
|
422
|
-
function checkAutoExceptions(value, ctx, node) {
|
|
423
|
-
if (typeof value !== 'string' || ctx.autoExceptions.length === 0) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
for (const pattern of ctx.autoExceptions) {
|
|
427
|
-
if (pattern.test(value)) {
|
|
428
|
-
throw new AutoExceptionError(pattern.source, value, getNodeLocation(node));
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
// ============================================================
|
|
433
|
-
// TIMEOUT WRAPPER
|
|
434
|
-
// ============================================================
|
|
435
|
-
/**
|
|
436
|
-
* Wrap a promise with a timeout. Returns original promise if no timeout configured.
|
|
437
|
-
*/
|
|
438
|
-
function withTimeout(promise, timeoutMs, functionName, node) {
|
|
439
|
-
if (timeoutMs === undefined) {
|
|
440
|
-
return promise;
|
|
441
|
-
}
|
|
442
|
-
return Promise.race([
|
|
443
|
-
promise,
|
|
444
|
-
new Promise((_, reject) => {
|
|
445
|
-
setTimeout(() => {
|
|
446
|
-
reject(new TimeoutError(functionName, timeoutMs, getNodeLocation(node)));
|
|
447
|
-
}, timeoutMs);
|
|
448
|
-
}),
|
|
449
|
-
]);
|
|
450
|
-
}
|
|
451
|
-
// ============================================================
|
|
452
|
-
// ABORT CHECKING
|
|
453
|
-
// ============================================================
|
|
454
|
-
/**
|
|
455
|
-
* Check if execution has been aborted via AbortSignal.
|
|
456
|
-
* Throws AbortError if signal is aborted.
|
|
457
|
-
*/
|
|
458
|
-
function checkAborted(ctx, node) {
|
|
459
|
-
if (ctx.signal?.aborted) {
|
|
460
|
-
throw new AbortError(getNodeLocation(node));
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
// ============================================================
|
|
464
|
-
// INTERPRETER
|
|
465
|
-
// ============================================================
|
|
466
|
-
export async function execute(script, context) {
|
|
467
|
-
const stepper = createStepper(script, context);
|
|
468
|
-
while (!stepper.done) {
|
|
469
|
-
await stepper.step();
|
|
470
|
-
}
|
|
471
|
-
return stepper.getResult();
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Create a stepper for controlled step-by-step execution.
|
|
475
|
-
* Allows the caller to control the execution loop and inspect state between steps.
|
|
476
|
-
*/
|
|
477
|
-
export function createStepper(script, context) {
|
|
478
|
-
const statements = script.statements;
|
|
479
|
-
const total = statements.length;
|
|
480
|
-
let index = 0;
|
|
481
|
-
let lastValue = null;
|
|
482
|
-
let isDone = total === 0;
|
|
483
|
-
const collectVariables = () => {
|
|
484
|
-
const vars = {};
|
|
485
|
-
for (const [name, value] of context.variables) {
|
|
486
|
-
vars[name] = value;
|
|
487
|
-
}
|
|
488
|
-
return vars;
|
|
489
|
-
};
|
|
490
|
-
return {
|
|
491
|
-
get done() {
|
|
492
|
-
return isDone;
|
|
493
|
-
},
|
|
494
|
-
get index() {
|
|
495
|
-
return index;
|
|
496
|
-
},
|
|
497
|
-
get total() {
|
|
498
|
-
return total;
|
|
499
|
-
},
|
|
500
|
-
get context() {
|
|
501
|
-
return context;
|
|
502
|
-
},
|
|
503
|
-
async step() {
|
|
504
|
-
if (isDone) {
|
|
505
|
-
return {
|
|
506
|
-
value: lastValue,
|
|
507
|
-
done: true,
|
|
508
|
-
index: index,
|
|
509
|
-
total,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
const stmt = statements[index];
|
|
513
|
-
if (!stmt) {
|
|
514
|
-
isDone = true;
|
|
515
|
-
return { value: lastValue, done: true, index, total };
|
|
516
|
-
}
|
|
517
|
-
// Check for abort before each step
|
|
518
|
-
checkAborted(context, stmt);
|
|
519
|
-
const startTime = Date.now();
|
|
520
|
-
// Fire onStepStart
|
|
521
|
-
context.observability.onStepStart?.({
|
|
522
|
-
index,
|
|
523
|
-
total,
|
|
524
|
-
pipeValue: context.pipeValue,
|
|
525
|
-
});
|
|
526
|
-
let captured;
|
|
527
|
-
try {
|
|
528
|
-
// Execute the statement
|
|
529
|
-
const value = await evaluateExpression(stmt.expression, context);
|
|
530
|
-
// Handle capture: -> $varname or -> $varname:type
|
|
531
|
-
if (stmt.capture) {
|
|
532
|
-
setVariable(context, stmt.capture.name, value, stmt.capture.typeName, stmt.capture.span.start);
|
|
533
|
-
captured = { name: stmt.capture.name, value };
|
|
534
|
-
context.observability.onCapture?.(captured);
|
|
535
|
-
}
|
|
536
|
-
// Update pipe value
|
|
537
|
-
context.pipeValue = value;
|
|
538
|
-
lastValue = value;
|
|
539
|
-
// Check auto-exceptions
|
|
540
|
-
checkAutoExceptions(value, context, stmt);
|
|
541
|
-
// Fire onStepEnd
|
|
542
|
-
context.observability.onStepEnd?.({
|
|
543
|
-
index,
|
|
544
|
-
total,
|
|
545
|
-
value,
|
|
546
|
-
durationMs: Date.now() - startTime,
|
|
547
|
-
});
|
|
548
|
-
index++;
|
|
549
|
-
isDone = index >= total;
|
|
550
|
-
return {
|
|
551
|
-
value,
|
|
552
|
-
done: isDone,
|
|
553
|
-
index: index - 1,
|
|
554
|
-
total,
|
|
555
|
-
captured,
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
catch (error) {
|
|
559
|
-
// Fire onError
|
|
560
|
-
context.observability.onError?.({
|
|
561
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
562
|
-
index,
|
|
563
|
-
});
|
|
564
|
-
throw error;
|
|
565
|
-
}
|
|
566
|
-
},
|
|
567
|
-
getResult() {
|
|
568
|
-
return {
|
|
569
|
-
value: lastValue,
|
|
570
|
-
variables: collectVariables(),
|
|
571
|
-
};
|
|
572
|
-
},
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
async function executeStatement(stmt, ctx) {
|
|
576
|
-
const value = await evaluateExpression(stmt.expression, ctx);
|
|
577
|
-
// Handle capture: -> $varname or -> $varname:type
|
|
578
|
-
if (stmt.capture) {
|
|
579
|
-
setVariable(ctx, stmt.capture.name, value, stmt.capture.typeName, stmt.capture.span.start);
|
|
580
|
-
ctx.observability.onCapture?.({ name: stmt.capture.name, value });
|
|
581
|
-
}
|
|
582
|
-
// Update pipe value for next statement
|
|
583
|
-
ctx.pipeValue = value;
|
|
584
|
-
// Check for auto-exceptions (halts execution if pattern matches)
|
|
585
|
-
checkAutoExceptions(value, ctx, stmt);
|
|
586
|
-
// Handle control flow terminators
|
|
587
|
-
if (stmt.terminator === 'break') {
|
|
588
|
-
throw new BreakSignal(value);
|
|
589
|
-
}
|
|
590
|
-
if (stmt.terminator === 'return') {
|
|
591
|
-
throw new ReturnSignal(value);
|
|
592
|
-
}
|
|
593
|
-
return value;
|
|
594
|
-
}
|
|
595
|
-
async function evaluateExpression(expr, ctx) {
|
|
596
|
-
return evaluatePipeChain(expr, ctx);
|
|
597
|
-
}
|
|
598
|
-
async function evaluatePipeChain(chain, ctx) {
|
|
599
|
-
// Evaluate head
|
|
600
|
-
let value = await evaluatePostfixExpr(chain.head, ctx);
|
|
601
|
-
ctx.pipeValue = value;
|
|
602
|
-
// Process each pipe target
|
|
603
|
-
for (const target of chain.pipes) {
|
|
604
|
-
value = await evaluatePipeTarget(target, value, ctx);
|
|
605
|
-
ctx.pipeValue = value;
|
|
606
|
-
}
|
|
607
|
-
return value;
|
|
608
|
-
}
|
|
609
|
-
async function evaluatePostfixExpr(expr, ctx) {
|
|
610
|
-
let value = await evaluatePrimary(expr.primary, ctx);
|
|
611
|
-
// Apply method chain
|
|
612
|
-
for (const method of expr.methods) {
|
|
613
|
-
value = await evaluateMethod(method, value, ctx);
|
|
614
|
-
}
|
|
615
|
-
return value;
|
|
616
|
-
}
|
|
617
|
-
async function evaluatePrimary(primary, ctx) {
|
|
618
|
-
switch (primary.type) {
|
|
619
|
-
case 'StringLiteral':
|
|
620
|
-
return evaluateString(primary, ctx);
|
|
621
|
-
case 'NumberLiteral':
|
|
622
|
-
return primary.value;
|
|
623
|
-
case 'BoolLiteral':
|
|
624
|
-
return primary.value;
|
|
625
|
-
case 'Tuple':
|
|
626
|
-
return evaluateTuple(primary, ctx);
|
|
627
|
-
case 'Dict':
|
|
628
|
-
return evaluateDict(primary, ctx);
|
|
629
|
-
case 'FunctionLiteral':
|
|
630
|
-
return await createClosure(primary, ctx);
|
|
631
|
-
case 'Variable':
|
|
632
|
-
return evaluateVariableAsync(primary, ctx);
|
|
633
|
-
case 'FunctionCall':
|
|
634
|
-
return evaluateFunctionCall(primary, ctx);
|
|
635
|
-
case 'VariableCall':
|
|
636
|
-
return evaluateVariableCall(primary, ctx);
|
|
637
|
-
case 'MethodCall':
|
|
638
|
-
// Bare method call: .method operates on current pipe value
|
|
639
|
-
return evaluateMethod(primary, ctx.pipeValue, ctx);
|
|
640
|
-
case 'Conditional':
|
|
641
|
-
return evaluateConditional(primary, ctx);
|
|
642
|
-
case 'WhileLoop':
|
|
643
|
-
return evaluateWhileLoop(primary, ctx);
|
|
644
|
-
case 'ForLoop':
|
|
645
|
-
return evaluateForLoop(primary, ctx);
|
|
646
|
-
case 'Block':
|
|
647
|
-
// Blocks execute immediately in normal context
|
|
648
|
-
return evaluateBlockExpression(primary, ctx);
|
|
649
|
-
case 'Arithmetic':
|
|
650
|
-
return evaluateArithmetic(primary, ctx);
|
|
651
|
-
case 'Spread':
|
|
652
|
-
return evaluateSpread(primary, ctx);
|
|
653
|
-
default:
|
|
654
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown primary type: ${primary.type}`, getNodeLocation(primary));
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
async function evaluatePipeTarget(target, input, ctx) {
|
|
658
|
-
ctx.pipeValue = input;
|
|
659
|
-
switch (target.type) {
|
|
660
|
-
case 'Capture':
|
|
661
|
-
// Inline capture: store value and pass through (like implicit .set())
|
|
662
|
-
return evaluateCapture(target, input, ctx);
|
|
663
|
-
case 'FunctionCall':
|
|
664
|
-
return evaluateFunctionCall(target, ctx);
|
|
665
|
-
case 'VariableCall':
|
|
666
|
-
// Pipe-style: if no args, pass piped value as first arg
|
|
667
|
-
return evaluateVariableCallWithPipe(target, input, ctx);
|
|
668
|
-
case 'Invoke':
|
|
669
|
-
return evaluateInvoke(target, input, ctx);
|
|
670
|
-
case 'MethodCall':
|
|
671
|
-
return evaluateMethod(target, input, ctx);
|
|
672
|
-
case 'Conditional':
|
|
673
|
-
return evaluateConditional(target, ctx);
|
|
674
|
-
case 'WhileLoop':
|
|
675
|
-
return evaluateWhileLoop(target, ctx);
|
|
676
|
-
case 'ForLoop':
|
|
677
|
-
return evaluateForLoop(target, ctx);
|
|
678
|
-
case 'Block':
|
|
679
|
-
return evaluateBlockExpression(target, ctx);
|
|
680
|
-
case 'StringLiteral':
|
|
681
|
-
return evaluateString(target, ctx);
|
|
682
|
-
case 'Arithmetic':
|
|
683
|
-
return evaluateArithmetic(target, ctx);
|
|
684
|
-
case 'ParallelSpread':
|
|
685
|
-
return evaluateParallelSpread(target, input, ctx);
|
|
686
|
-
case 'ParallelFilter':
|
|
687
|
-
return evaluateParallelFilter(target, input, ctx);
|
|
688
|
-
case 'SequentialSpread':
|
|
689
|
-
return evaluateSequentialSpread(target, input, ctx);
|
|
690
|
-
case 'Destructure':
|
|
691
|
-
return evaluateDestructure(target, input, ctx);
|
|
692
|
-
case 'Slice':
|
|
693
|
-
return evaluateSlice(target, input, ctx);
|
|
694
|
-
case 'Enumerate':
|
|
695
|
-
return evaluateEnumerate(target, input);
|
|
696
|
-
case 'Spread':
|
|
697
|
-
return evaluateSpread(target, ctx);
|
|
698
|
-
default:
|
|
699
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown pipe target type: ${target.type}`, getNodeLocation(target));
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
// ============================================================
|
|
703
|
-
// SPREAD OPERATIONS
|
|
704
|
-
// ============================================================
|
|
705
|
-
/**
|
|
706
|
-
* Evaluate parallel spread: $args -> ~$closures
|
|
707
|
-
*
|
|
708
|
-
* Broadcasting rules:
|
|
709
|
-
* - Zip: [a,b,c] -> ~[f,g,h] → f(a), g(b), h(c) in parallel
|
|
710
|
-
* - Map: [a,b,c] -> ~f → f(a), f(b), f(c) in parallel
|
|
711
|
-
* - Broadcast: x -> ~[f,g,h] → f(x), g(x), h(x) in parallel
|
|
712
|
-
*/
|
|
713
|
-
async function evaluateParallelSpread(node, input, ctx) {
|
|
714
|
-
// Evaluate the target expression to get closure(s)
|
|
715
|
-
const target = await evaluateExpression(node.target, ctx);
|
|
716
|
-
const inputArray = Array.isArray(input) ? input : null;
|
|
717
|
-
const targetArray = Array.isArray(target) ? target : null;
|
|
718
|
-
// Determine which broadcasting mode to use
|
|
719
|
-
if (inputArray && targetArray) {
|
|
720
|
-
// Zip mode: [a,b,c] -> ~[f,g,h]
|
|
721
|
-
if (inputArray.length !== targetArray.length) {
|
|
722
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Parallel zip requires equal lengths: got ${inputArray.length} args and ${targetArray.length} closures`, node.span.start);
|
|
723
|
-
}
|
|
724
|
-
const promises = inputArray.map((arg, i) => {
|
|
725
|
-
const closure = targetArray[i];
|
|
726
|
-
if (closure === undefined) {
|
|
727
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Missing closure at index ${i}`, node.span.start);
|
|
728
|
-
}
|
|
729
|
-
return invokeAsClosureOrFunction(closure, [arg], ctx, node.span.start);
|
|
730
|
-
});
|
|
731
|
-
return Promise.all(promises);
|
|
732
|
-
}
|
|
733
|
-
else if (inputArray && !targetArray) {
|
|
734
|
-
// Map mode: [a,b,c] -> ~f
|
|
735
|
-
const promises = inputArray.map((arg) => invokeAsClosureOrFunction(target, [arg], ctx, node.span.start));
|
|
736
|
-
return Promise.all(promises);
|
|
737
|
-
}
|
|
738
|
-
else if (!inputArray && targetArray) {
|
|
739
|
-
// Broadcast mode: x -> ~[f,g,h]
|
|
740
|
-
const promises = targetArray.map((closure) => invokeAsClosureOrFunction(closure, [input], ctx, node.span.start));
|
|
741
|
-
return Promise.all(promises);
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
// Single closure, single arg: just invoke
|
|
745
|
-
const result = await invokeAsClosureOrFunction(target, [input], ctx, node.span.start);
|
|
746
|
-
return [result];
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Evaluate parallel filter: $tuple -> ~?{ condition } or ~?$predicate
|
|
751
|
-
* Keeps elements where predicate returns truthy.
|
|
752
|
-
* Inside the block, $ binds to the current element.
|
|
753
|
-
*/
|
|
754
|
-
async function evaluateParallelFilter(node, input, ctx) {
|
|
755
|
-
// Input must be iterable (tuple)
|
|
756
|
-
if (!Array.isArray(input)) {
|
|
757
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Filter requires tuple, got ${isDict(input) ? 'dict' : typeof input}`, node.span.start);
|
|
758
|
-
}
|
|
759
|
-
const results = [];
|
|
760
|
-
// Evaluate predicate for each element
|
|
761
|
-
for (const element of input) {
|
|
762
|
-
// Set $ to current element
|
|
763
|
-
const savedPipeValue = ctx.pipeValue;
|
|
764
|
-
ctx.pipeValue = element;
|
|
765
|
-
let predicateResult;
|
|
766
|
-
if (node.predicate.type === 'Block') {
|
|
767
|
-
// Block form: ~?{ .gt(2) }
|
|
768
|
-
predicateResult = await evaluateBlockExpression(node.predicate, ctx);
|
|
769
|
-
}
|
|
770
|
-
else {
|
|
771
|
-
// Variable form: ~?$pred - invoke closure with element as arg
|
|
772
|
-
const closure = ctx.variables.get(node.predicate.name ?? '');
|
|
773
|
-
if (!closure) {
|
|
774
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, `Undefined variable: $${node.predicate.name}`, node.predicate.span.start, { variableName: node.predicate.name });
|
|
775
|
-
}
|
|
776
|
-
if (!isCallable(closure)) {
|
|
777
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Filter predicate must be callable, got ${typeof closure}`, node.predicate.span.start);
|
|
778
|
-
}
|
|
779
|
-
predicateResult = await invokeCallable(closure, [element], ctx, node.predicate.span.start);
|
|
780
|
-
}
|
|
781
|
-
// Keep element if predicate is truthy
|
|
782
|
-
if (isTruthy(predicateResult)) {
|
|
783
|
-
results.push(element);
|
|
784
|
-
}
|
|
785
|
-
// Restore pipe value
|
|
786
|
-
ctx.pipeValue = savedPipeValue;
|
|
787
|
-
}
|
|
788
|
-
return results;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Evaluate sequential spread: $input -> @$closures
|
|
792
|
-
* Chains closures where each receives the previous result (fold).
|
|
793
|
-
*/
|
|
794
|
-
async function evaluateSequentialSpread(node, input, ctx) {
|
|
795
|
-
// Evaluate the target expression to get closure(s)
|
|
796
|
-
const target = await evaluateExpression(node.target, ctx);
|
|
797
|
-
const closures = Array.isArray(target) ? target : [target];
|
|
798
|
-
let accumulated = input;
|
|
799
|
-
for (const closure of closures) {
|
|
800
|
-
accumulated = await invokeAsClosureOrFunction(closure, [accumulated], ctx, node.span.start);
|
|
801
|
-
}
|
|
802
|
-
return accumulated;
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Invoke a value as either a callable or look it up as a function name.
|
|
806
|
-
* This enables uniform handling in spread operations.
|
|
807
|
-
*/
|
|
808
|
-
async function invokeAsCallableOrFunction(callableOrName, args, ctx, location) {
|
|
809
|
-
// If it's any callable (script or runtime), invoke it
|
|
810
|
-
if (isCallable(callableOrName)) {
|
|
811
|
-
return invokeCallable(callableOrName, args, ctx, location);
|
|
812
|
-
}
|
|
813
|
-
// If it's a string, try to look it up as a function name
|
|
814
|
-
if (typeof callableOrName === 'string') {
|
|
815
|
-
const fn = ctx.functions.get(callableOrName);
|
|
816
|
-
if (fn) {
|
|
817
|
-
const result = fn(args, ctx, location);
|
|
818
|
-
return result instanceof Promise ? result : result;
|
|
819
|
-
}
|
|
820
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_FUNCTION, `Unknown function: ${callableOrName}`, location, { functionName: callableOrName });
|
|
821
|
-
}
|
|
822
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Expected callable or function name, got ${typeof callableOrName}`, location);
|
|
823
|
-
}
|
|
824
|
-
// Legacy alias
|
|
825
|
-
const invokeAsClosureOrFunction = invokeAsCallableOrFunction;
|
|
826
|
-
/**
|
|
827
|
-
* Evaluate inline capture: store value and return unchanged (pass-through).
|
|
828
|
-
* Semantically: "-> $a ->" ≡ "-> $a.set($) ->"
|
|
829
|
-
*/
|
|
830
|
-
function evaluateCapture(node, input, ctx) {
|
|
831
|
-
setVariable(ctx, node.name, input, node.typeName, node.span.start);
|
|
832
|
-
ctx.observability.onCapture?.({ name: node.name, value: input });
|
|
833
|
-
return input; // Identity pass-through
|
|
834
|
-
}
|
|
835
|
-
// ============================================================
|
|
836
|
-
// EXTRACTION OPERATORS
|
|
837
|
-
// ============================================================
|
|
838
|
-
/**
|
|
839
|
-
* Evaluate destructure: :<$a, $b, $c> or :<key: $var>
|
|
840
|
-
* Extracts elements from tuples/dicts into variables.
|
|
841
|
-
* Returns the original input unchanged.
|
|
842
|
-
*/
|
|
843
|
-
function evaluateDestructure(node, input, ctx) {
|
|
844
|
-
const isTuple = Array.isArray(input);
|
|
845
|
-
const isDictInput = isDict(input);
|
|
846
|
-
// Determine pattern type from first non-skip element
|
|
847
|
-
const firstNonSkip = node.elements.find((e) => e.kind !== 'skip');
|
|
848
|
-
const isKeyPattern = firstNonSkip?.kind === 'keyValue';
|
|
849
|
-
if (isKeyPattern) {
|
|
850
|
-
// Dict destructuring: :<key: $var, ...>
|
|
851
|
-
if (!isDictInput) {
|
|
852
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Key destructure requires dict, got ${isTuple ? 'tuple' : typeof input}`, node.span.start);
|
|
853
|
-
}
|
|
854
|
-
for (const elem of node.elements) {
|
|
855
|
-
if (elem.kind === 'skip')
|
|
856
|
-
continue;
|
|
857
|
-
if (elem.kind === 'nested') {
|
|
858
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Nested destructure not supported in dict patterns', elem.span.start);
|
|
859
|
-
}
|
|
860
|
-
if (elem.kind !== 'keyValue' || elem.key === null || elem.name === null) {
|
|
861
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Dict destructure requires key: $var patterns', elem.span.start);
|
|
862
|
-
}
|
|
863
|
-
const dictInput = input;
|
|
864
|
-
if (!(elem.key in dictInput)) {
|
|
865
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Key '${elem.key}' not found in dict`, elem.span.start, { key: elem.key, availableKeys: Object.keys(dictInput) });
|
|
866
|
-
}
|
|
867
|
-
const dictValue = dictInput[elem.key];
|
|
868
|
-
if (dictValue === undefined) {
|
|
869
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Key '${elem.key}' has undefined value`, elem.span.start);
|
|
870
|
-
}
|
|
871
|
-
setVariable(ctx, elem.name, dictValue, elem.typeName, elem.span.start);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
else {
|
|
875
|
-
// Tuple destructuring: :<$a, $b, $c>
|
|
876
|
-
if (!isTuple) {
|
|
877
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Positional destructure requires tuple, got ${isDictInput ? 'dict' : typeof input}`, node.span.start);
|
|
878
|
-
}
|
|
879
|
-
const tupleInput = input;
|
|
880
|
-
if (node.elements.length !== tupleInput.length) {
|
|
881
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Destructure pattern has ${node.elements.length} elements, tuple has ${tupleInput.length}`, node.span.start);
|
|
882
|
-
}
|
|
883
|
-
for (let i = 0; i < node.elements.length; i++) {
|
|
884
|
-
const elem = node.elements[i];
|
|
885
|
-
const value = tupleInput[i];
|
|
886
|
-
if (elem === undefined || value === undefined) {
|
|
887
|
-
continue; // Should not happen due to length check above
|
|
888
|
-
}
|
|
889
|
-
if (elem.kind === 'skip')
|
|
890
|
-
continue;
|
|
891
|
-
if (elem.kind === 'nested' && elem.nested) {
|
|
892
|
-
// Recursively destructure nested value
|
|
893
|
-
evaluateDestructure(elem.nested, value, ctx);
|
|
894
|
-
continue;
|
|
895
|
-
}
|
|
896
|
-
if (elem.name === null) {
|
|
897
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Invalid destructure element', elem.span.start);
|
|
898
|
-
}
|
|
899
|
-
setVariable(ctx, elem.name, value, elem.typeName, elem.span.start);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
return input; // Return original input unchanged
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Evaluate slice: /<start:stop:step>
|
|
906
|
-
* Extracts a portion of tuple or string using Python-style slicing.
|
|
907
|
-
*/
|
|
908
|
-
function evaluateSlice(node, input, ctx) {
|
|
909
|
-
const isTuple = Array.isArray(input);
|
|
910
|
-
const isString = typeof input === 'string';
|
|
911
|
-
if (!isTuple && !isString) {
|
|
912
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Slice requires tuple or string, got ${isDict(input) ? 'dict' : typeof input}`, node.span.start);
|
|
913
|
-
}
|
|
914
|
-
// Evaluate bounds
|
|
915
|
-
const startBound = node.start ? evaluateSliceBound(node.start, ctx) : null;
|
|
916
|
-
const stopBound = node.stop ? evaluateSliceBound(node.stop, ctx) : null;
|
|
917
|
-
const stepBound = node.step ? evaluateSliceBound(node.step, ctx) : null;
|
|
918
|
-
// Apply Python slice semantics based on input type
|
|
919
|
-
if (isTuple) {
|
|
920
|
-
return applySlice(input, input.length, startBound, stopBound, stepBound);
|
|
921
|
-
}
|
|
922
|
-
// isString is true at this point
|
|
923
|
-
return applySlice(input, input.length, startBound, stopBound, stepBound);
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Evaluate a slice bound to a number
|
|
927
|
-
*/
|
|
928
|
-
function evaluateSliceBound(bound, ctx) {
|
|
929
|
-
if (bound === null) {
|
|
930
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Slice bound is null', undefined);
|
|
931
|
-
}
|
|
932
|
-
switch (bound.type) {
|
|
933
|
-
case 'NumberLiteral':
|
|
934
|
-
return bound.value;
|
|
935
|
-
case 'Variable': {
|
|
936
|
-
const value = evaluateVariable(bound, ctx);
|
|
937
|
-
if (typeof value !== 'number') {
|
|
938
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Slice bound must be number, got ${typeof value}`, bound.span.start);
|
|
939
|
-
}
|
|
940
|
-
return value;
|
|
941
|
-
}
|
|
942
|
-
case 'Arithmetic':
|
|
943
|
-
return evaluateArithmetic(bound, ctx);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Apply Python-style slice semantics
|
|
948
|
-
*/
|
|
949
|
-
function applySlice(input, len, start, stop, step) {
|
|
950
|
-
const actualStep = step ?? 1;
|
|
951
|
-
if (actualStep === 0) {
|
|
952
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Slice step cannot be zero', undefined);
|
|
953
|
-
}
|
|
954
|
-
// Normalize indices (handle negatives, clamp to bounds)
|
|
955
|
-
const normalizeIndex = (idx, defaultVal, forStep) => {
|
|
956
|
-
if (idx === null)
|
|
957
|
-
return defaultVal;
|
|
958
|
-
let normalized = idx < 0 ? len + idx : idx;
|
|
959
|
-
// Clamp to valid range based on step direction
|
|
960
|
-
if (forStep > 0) {
|
|
961
|
-
normalized = Math.max(0, Math.min(len, normalized));
|
|
962
|
-
}
|
|
963
|
-
else {
|
|
964
|
-
normalized = Math.max(-1, Math.min(len - 1, normalized));
|
|
965
|
-
}
|
|
966
|
-
return normalized;
|
|
967
|
-
};
|
|
968
|
-
const actualStart = normalizeIndex(start, actualStep > 0 ? 0 : len - 1, actualStep);
|
|
969
|
-
const actualStop = normalizeIndex(stop, actualStep > 0 ? len : -1, actualStep);
|
|
970
|
-
// Collect indices
|
|
971
|
-
const indices = [];
|
|
972
|
-
if (actualStep > 0) {
|
|
973
|
-
for (let i = actualStart; i < actualStop; i += actualStep) {
|
|
974
|
-
indices.push(i);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
for (let i = actualStart; i > actualStop; i += actualStep) {
|
|
979
|
-
indices.push(i);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
// Extract elements
|
|
983
|
-
if (Array.isArray(input)) {
|
|
984
|
-
return indices.map((i) => input[i]);
|
|
985
|
-
}
|
|
986
|
-
else {
|
|
987
|
-
return indices.map((i) => input[i]).join('');
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
/**
|
|
991
|
-
* Evaluate spread: *expr or -> *
|
|
992
|
-
* Converts tuple or dict to args type for unpacking at closure invocation.
|
|
993
|
-
*/
|
|
994
|
-
async function evaluateSpread(node, ctx) {
|
|
995
|
-
// Get the value to spread
|
|
996
|
-
let value;
|
|
997
|
-
if (node.operand === null) {
|
|
998
|
-
// Pipe target form: -> * (use current pipe value)
|
|
999
|
-
value = ctx.pipeValue;
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
// Prefix form: *expr
|
|
1003
|
-
value = await evaluateExpression(node.operand, ctx);
|
|
1004
|
-
}
|
|
1005
|
-
// Convert to args based on type
|
|
1006
|
-
if (Array.isArray(value)) {
|
|
1007
|
-
return createArgsFromTuple(value);
|
|
1008
|
-
}
|
|
1009
|
-
if (isDict(value)) {
|
|
1010
|
-
return createArgsFromDict(value);
|
|
1011
|
-
}
|
|
1012
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Spread requires tuple or dict, got ${inferType(value)}`, node.span.start);
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Evaluate enumerate: @<>
|
|
1016
|
-
* Transforms tuple/dict into tuple of dicts with index/value (and key for dicts).
|
|
1017
|
-
*/
|
|
1018
|
-
function evaluateEnumerate(node, input) {
|
|
1019
|
-
if (Array.isArray(input)) {
|
|
1020
|
-
// Tuple enumeration: [[index: 0, value: x], ...]
|
|
1021
|
-
return input.map((value, index) => ({
|
|
1022
|
-
index,
|
|
1023
|
-
value,
|
|
1024
|
-
}));
|
|
1025
|
-
}
|
|
1026
|
-
if (isDict(input)) {
|
|
1027
|
-
// Dict enumeration: [[index: 0, key: "k", value: v], ...]
|
|
1028
|
-
// Keys sorted alphabetically for deterministic output
|
|
1029
|
-
const keys = Object.keys(input).sort();
|
|
1030
|
-
return keys.map((key, index) => ({
|
|
1031
|
-
index,
|
|
1032
|
-
key,
|
|
1033
|
-
value: input[key],
|
|
1034
|
-
}));
|
|
1035
|
-
}
|
|
1036
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Enumerate requires tuple or dict, got ${typeof input}`, node.span.start);
|
|
1037
|
-
}
|
|
1038
|
-
// ============================================================
|
|
1039
|
-
// LITERAL EVALUATION
|
|
1040
|
-
// ============================================================
|
|
1041
|
-
async function evaluateString(node, ctx) {
|
|
1042
|
-
// Handle interpolation
|
|
1043
|
-
let result = '';
|
|
1044
|
-
for (const part of node.parts) {
|
|
1045
|
-
if (typeof part === 'string') {
|
|
1046
|
-
result += part;
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
// Interpolation node - for now just use the expression
|
|
1050
|
-
// TODO: Properly handle interpolation nodes when lexer supports them
|
|
1051
|
-
result += formatValue(ctx.pipeValue);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
// Handle {$fn(args)} patterns - variable calls (must come before {$name} pattern)
|
|
1055
|
-
// Supports: {$fn()}, {$fn("str")}, {$fn(123)}, {$fn($var)}, {$fn($)}
|
|
1056
|
-
const varCallPattern = /\{\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\(\s*([^)]*)\s*\)\s*\}/g;
|
|
1057
|
-
const varCallMatches = [...result.matchAll(varCallPattern)];
|
|
1058
|
-
for (const match of varCallMatches.reverse()) {
|
|
1059
|
-
// Process in reverse to preserve indices
|
|
1060
|
-
const fullMatch = match[0];
|
|
1061
|
-
const fnName = match[1] ?? '';
|
|
1062
|
-
const argsStr = match[2] ?? '';
|
|
1063
|
-
const closure = ctx.variables.get(fnName);
|
|
1064
|
-
if (closure && isCallable(closure)) {
|
|
1065
|
-
const args = parseInterpolationArgs(argsStr, ctx);
|
|
1066
|
-
const callResult = await invokeCallable(closure, args, ctx, node.span.start);
|
|
1067
|
-
result =
|
|
1068
|
-
result.slice(0, match.index) +
|
|
1069
|
-
formatValue(callResult) +
|
|
1070
|
-
result.slice(match.index + fullMatch.length);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
// Handle {$} and {$.field} patterns - pipe variable
|
|
1074
|
-
result = result.replace(/\{\s*\$(?![a-zA-Z_])([^}]*)\}/g, (_match, field) => {
|
|
1075
|
-
let value = ctx.pipeValue;
|
|
1076
|
-
const trimmed = field.trim();
|
|
1077
|
-
if (trimmed) {
|
|
1078
|
-
value = accessField(value, trimmed.slice(1)); // Remove leading .
|
|
1079
|
-
}
|
|
1080
|
-
return formatValue(value);
|
|
1081
|
-
});
|
|
1082
|
-
// Handle {$name} and {$name.field} patterns - named variables
|
|
1083
|
-
// If the variable is a closure, auto-invoke it (with $ as arg if it has params)
|
|
1084
|
-
const varPattern = /\{\s*\$([a-zA-Z_][a-zA-Z0-9_]*)([^}]*)\}/g;
|
|
1085
|
-
const varMatches = [...result.matchAll(varPattern)];
|
|
1086
|
-
for (const match of varMatches.reverse()) {
|
|
1087
|
-
const fullMatch = match[0];
|
|
1088
|
-
const name = match[1] ?? '';
|
|
1089
|
-
const field = match[2] ?? '';
|
|
1090
|
-
const idx = match.index;
|
|
1091
|
-
if (!ctx.variables.has(name)) {
|
|
1092
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, `Undefined variable: $${name}`, getNodeLocation(node), { variableName: name });
|
|
1093
|
-
}
|
|
1094
|
-
let value = ctx.variables.get(name) ?? null;
|
|
1095
|
-
// Auto-invoke closures
|
|
1096
|
-
if (isScriptCallable(value)) {
|
|
1097
|
-
const args = value.params.length > 0 ? [ctx.pipeValue] : [];
|
|
1098
|
-
value = await invokeScriptCallable(value, args, ctx, node.span.start);
|
|
1099
|
-
}
|
|
1100
|
-
const trimmed = field.trim();
|
|
1101
|
-
if (trimmed) {
|
|
1102
|
-
value = accessField(value, trimmed.slice(1)); // Remove leading .
|
|
1103
|
-
}
|
|
1104
|
-
result =
|
|
1105
|
-
result.slice(0, idx) +
|
|
1106
|
-
formatValue(value) +
|
|
1107
|
-
result.slice(idx + fullMatch.length);
|
|
1108
|
-
}
|
|
1109
|
-
// Handle {.method} patterns - call method on current pipe value
|
|
1110
|
-
// Note: String interpolation only supports sync methods (built-ins)
|
|
1111
|
-
result = result.replace(/\{\s*\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}/g, (_match, methodName) => {
|
|
1112
|
-
const method = BUILTIN_METHODS[methodName];
|
|
1113
|
-
if (method) {
|
|
1114
|
-
// Built-in methods are synchronous, so we can safely cast
|
|
1115
|
-
const methodResult = method(ctx.pipeValue, [], ctx);
|
|
1116
|
-
return formatValue(methodResult);
|
|
1117
|
-
}
|
|
1118
|
-
return `{.${methodName}}`; // Return unchanged if method not found
|
|
1119
|
-
});
|
|
1120
|
-
return result;
|
|
1121
|
-
}
|
|
1122
|
-
/** Parse simple arguments from string interpolation: "str", 123, $var, $ */
|
|
1123
|
-
function parseInterpolationArgs(argsStr, ctx) {
|
|
1124
|
-
const trimmed = argsStr.trim();
|
|
1125
|
-
if (!trimmed)
|
|
1126
|
-
return [];
|
|
1127
|
-
const args = [];
|
|
1128
|
-
// Simple comma-split (doesn't handle strings with commas, but good enough for now)
|
|
1129
|
-
const parts = trimmed.split(',').map((p) => p.trim());
|
|
1130
|
-
for (const part of parts) {
|
|
1131
|
-
if (part.startsWith('"') && part.endsWith('"')) {
|
|
1132
|
-
// String literal
|
|
1133
|
-
args.push(part.slice(1, -1));
|
|
1134
|
-
}
|
|
1135
|
-
else if (/^-?\d+(\.\d+)?$/.test(part)) {
|
|
1136
|
-
// Number literal
|
|
1137
|
-
args.push(parseFloat(part));
|
|
1138
|
-
}
|
|
1139
|
-
else if (part === '$') {
|
|
1140
|
-
// Pipe variable
|
|
1141
|
-
args.push(ctx.pipeValue);
|
|
1142
|
-
}
|
|
1143
|
-
else if (part.startsWith('$')) {
|
|
1144
|
-
// Named variable
|
|
1145
|
-
const varName = part.slice(1);
|
|
1146
|
-
args.push(ctx.variables.get(varName) ?? null);
|
|
1147
|
-
}
|
|
1148
|
-
else if (part === 'true') {
|
|
1149
|
-
args.push(true);
|
|
1150
|
-
}
|
|
1151
|
-
else if (part === 'false') {
|
|
1152
|
-
args.push(false);
|
|
1153
|
-
}
|
|
1154
|
-
else {
|
|
1155
|
-
// Unknown - treat as string
|
|
1156
|
-
args.push(part);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
return args;
|
|
1160
|
-
}
|
|
1161
|
-
async function evaluateTuple(node, ctx) {
|
|
1162
|
-
const elements = [];
|
|
1163
|
-
for (const elem of node.elements) {
|
|
1164
|
-
elements.push(await evaluateExpression(elem, ctx));
|
|
1165
|
-
}
|
|
1166
|
-
return elements;
|
|
1167
|
-
}
|
|
1168
|
-
/**
|
|
1169
|
-
* Check if an expression is a function literal: () { } or (params) { }
|
|
1170
|
-
* Used for dict closure detection - only function literals become dict closures.
|
|
1171
|
-
*/
|
|
1172
|
-
function isFunctionLiteralExpr(expr) {
|
|
1173
|
-
if (expr.pipes.length > 0)
|
|
1174
|
-
return false;
|
|
1175
|
-
if (expr.head.methods.length > 0)
|
|
1176
|
-
return false;
|
|
1177
|
-
return expr.head.primary.type === 'FunctionLiteral';
|
|
1178
|
-
}
|
|
1179
|
-
async function evaluateDict(node, ctx) {
|
|
1180
|
-
const result = {};
|
|
1181
|
-
for (const entry of node.entries) {
|
|
1182
|
-
// Check for reserved method names
|
|
1183
|
-
if (isReservedMethod(entry.key)) {
|
|
1184
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot use reserved method name '${entry.key}' as dict key`, entry.span.start, { key: entry.key, reservedMethods: RESERVED_DICT_METHODS });
|
|
1185
|
-
}
|
|
1186
|
-
// Function literals in dicts become dict closures with late-bound $
|
|
1187
|
-
// Syntax: [key: () { $.name }] or [key: (x) { $x }]
|
|
1188
|
-
if (isFunctionLiteralExpr(entry.value)) {
|
|
1189
|
-
const fnLit = entry.value.head.primary;
|
|
1190
|
-
const closure = await createClosure(fnLit, ctx);
|
|
1191
|
-
result[entry.key] = closure;
|
|
1192
|
-
}
|
|
1193
|
-
else {
|
|
1194
|
-
// Everything else (including bare blocks { }) executes immediately
|
|
1195
|
-
result[entry.key] = await evaluateExpression(entry.value, ctx);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
// Bind callables to this dict ($ = this for dict methods)
|
|
1199
|
-
for (const key of Object.keys(result)) {
|
|
1200
|
-
const value = result[key];
|
|
1201
|
-
if (value !== undefined && isCallable(value)) {
|
|
1202
|
-
// Create a new callable with boundDict set
|
|
1203
|
-
result[key] = {
|
|
1204
|
-
...value,
|
|
1205
|
-
boundDict: result,
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return result;
|
|
1210
|
-
}
|
|
1211
|
-
/** Create a closure from a function literal, capturing current variables */
|
|
1212
|
-
async function createClosure(node, ctx) {
|
|
1213
|
-
// Capture current variable bindings (shallow copy)
|
|
1214
|
-
const capturedVars = new Map(ctx.variables);
|
|
1215
|
-
// Convert FuncParamNode[] to CallableParam[], evaluating defaults
|
|
1216
|
-
const params = [];
|
|
1217
|
-
for (const param of node.params) {
|
|
1218
|
-
let defaultValue = null;
|
|
1219
|
-
if (param.defaultValue) {
|
|
1220
|
-
defaultValue = await evaluatePrimary(param.defaultValue, ctx);
|
|
1221
|
-
}
|
|
1222
|
-
params.push({
|
|
1223
|
-
name: param.name,
|
|
1224
|
-
typeName: param.typeName,
|
|
1225
|
-
defaultValue,
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
// Property-style: no params means auto-invoke when accessed from dict
|
|
1229
|
-
const isProperty = params.length === 0;
|
|
1230
|
-
return {
|
|
1231
|
-
__type: 'callable',
|
|
1232
|
-
kind: 'script',
|
|
1233
|
-
params,
|
|
1234
|
-
body: node.body,
|
|
1235
|
-
capturedVars,
|
|
1236
|
-
isProperty,
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
// ============================================================
|
|
1240
|
-
// VARIABLE EVALUATION
|
|
1241
|
-
// ============================================================
|
|
1242
|
-
/**
|
|
1243
|
-
* Evaluate a variable reference.
|
|
1244
|
-
* Note: This is synchronous but may return a closure that should be auto-invoked.
|
|
1245
|
-
* The caller should use evaluateVariableAsync for full dict closure support.
|
|
1246
|
-
*/
|
|
1247
|
-
function evaluateVariable(node, ctx) {
|
|
1248
|
-
let value;
|
|
1249
|
-
if (node.isPipeVar) {
|
|
1250
|
-
value = ctx.pipeValue;
|
|
1251
|
-
}
|
|
1252
|
-
else if (node.name) {
|
|
1253
|
-
value = ctx.variables.get(node.name) ?? null;
|
|
1254
|
-
}
|
|
1255
|
-
else {
|
|
1256
|
-
value = null;
|
|
1257
|
-
}
|
|
1258
|
-
// Apply field access
|
|
1259
|
-
for (const access of node.fieldAccess) {
|
|
1260
|
-
value = accessField(value, access.field);
|
|
1261
|
-
}
|
|
1262
|
-
return value;
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Evaluate a variable reference with async support for dict callables.
|
|
1266
|
-
* Auto-invokes property-style callables that are bound to a dict.
|
|
1267
|
-
*/
|
|
1268
|
-
async function evaluateVariableAsync(node, ctx) {
|
|
1269
|
-
let value;
|
|
1270
|
-
if (node.isPipeVar) {
|
|
1271
|
-
value = ctx.pipeValue;
|
|
1272
|
-
}
|
|
1273
|
-
else if (node.name) {
|
|
1274
|
-
value = ctx.variables.get(node.name) ?? null;
|
|
1275
|
-
}
|
|
1276
|
-
else {
|
|
1277
|
-
value = null;
|
|
1278
|
-
}
|
|
1279
|
-
// Apply field access
|
|
1280
|
-
for (const access of node.fieldAccess) {
|
|
1281
|
-
value = accessField(value, access.field);
|
|
1282
|
-
// Auto-invoke property-style callables bound to a dict
|
|
1283
|
-
if (isCallable(value) && value.isProperty && value.boundDict) {
|
|
1284
|
-
value = await invokeCallable(value, [], ctx, node.span.start);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
return value;
|
|
1288
|
-
}
|
|
1289
|
-
function accessField(value, field) {
|
|
1290
|
-
if (value === null)
|
|
1291
|
-
return null;
|
|
1292
|
-
if (typeof field === 'number') {
|
|
1293
|
-
if (Array.isArray(value))
|
|
1294
|
-
return value[field] ?? null;
|
|
1295
|
-
if (typeof value === 'string')
|
|
1296
|
-
return value[field] ?? '';
|
|
1297
|
-
return null;
|
|
1298
|
-
}
|
|
1299
|
-
// Don't allow field access on closures (they're opaque function values)
|
|
1300
|
-
if (typeof value === 'object' && !Array.isArray(value) && !isScriptCallable(value)) {
|
|
1301
|
-
return value[field] ?? null;
|
|
1302
|
-
}
|
|
1303
|
-
return null;
|
|
1304
|
-
}
|
|
1305
|
-
// ============================================================
|
|
1306
|
-
// FUNCTION & METHOD EVALUATION
|
|
1307
|
-
// ============================================================
|
|
1308
|
-
async function evaluateFunctionCall(node, ctx) {
|
|
1309
|
-
// Check for abort
|
|
1310
|
-
checkAborted(ctx, node);
|
|
1311
|
-
const fn = ctx.functions.get(node.name);
|
|
1312
|
-
if (!fn) {
|
|
1313
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_FUNCTION, `Unknown function: ${node.name}`, getNodeLocation(node), { functionName: node.name });
|
|
1314
|
-
}
|
|
1315
|
-
// Save pipeValue before evaluating arguments
|
|
1316
|
-
const savedPipeValue = ctx.pipeValue;
|
|
1317
|
-
// Evaluate arguments
|
|
1318
|
-
const args = [];
|
|
1319
|
-
for (const arg of node.args) {
|
|
1320
|
-
args.push(await evaluateExpression(arg, ctx));
|
|
1321
|
-
}
|
|
1322
|
-
// Restore pipeValue after argument evaluation
|
|
1323
|
-
ctx.pipeValue = savedPipeValue;
|
|
1324
|
-
// If no args provided and we have a pipe value, use it as first arg
|
|
1325
|
-
if (args.length === 0 && ctx.pipeValue !== null) {
|
|
1326
|
-
args.push(ctx.pipeValue);
|
|
1327
|
-
}
|
|
1328
|
-
// Fire onFunctionCall
|
|
1329
|
-
ctx.observability.onFunctionCall?.({ name: node.name, args });
|
|
1330
|
-
const startTime = Date.now();
|
|
1331
|
-
// Execute function with optional timeout, passing location
|
|
1332
|
-
const location = getNodeLocation(node);
|
|
1333
|
-
const result = fn(args, ctx, location);
|
|
1334
|
-
let value;
|
|
1335
|
-
if (result instanceof Promise) {
|
|
1336
|
-
value = await withTimeout(result, ctx.timeout, node.name, node);
|
|
1337
|
-
}
|
|
1338
|
-
else {
|
|
1339
|
-
value = result;
|
|
1340
|
-
}
|
|
1341
|
-
// Fire onFunctionReturn
|
|
1342
|
-
ctx.observability.onFunctionReturn?.({
|
|
1343
|
-
name: node.name,
|
|
1344
|
-
value,
|
|
1345
|
-
durationMs: Date.now() - startTime,
|
|
1346
|
-
});
|
|
1347
|
-
return value;
|
|
1348
|
-
}
|
|
1349
|
-
/**
|
|
1350
|
-
* Evaluate a variable call: $fn(args) - invokes closure stored in variable.
|
|
1351
|
-
* Follows implied $ pattern: $fn() with no args receives ctx.pipeValue as first arg
|
|
1352
|
-
* (same as .method() receiving $ implicitly).
|
|
1353
|
-
*/
|
|
1354
|
-
async function evaluateVariableCall(node, ctx) {
|
|
1355
|
-
return evaluateVariableCallWithPipe(node, ctx.pipeValue, ctx);
|
|
1356
|
-
}
|
|
1357
|
-
/**
|
|
1358
|
-
* Evaluate variable call with optional pipe value.
|
|
1359
|
-
* If pipeInput is provided and no args, passes it as first arg.
|
|
1360
|
-
*/
|
|
1361
|
-
async function evaluateVariableCallWithPipe(node, pipeInput, ctx) {
|
|
1362
|
-
const closure = ctx.variables.get(node.name);
|
|
1363
|
-
if (!closure) {
|
|
1364
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, `Unknown variable: $${node.name}`, getNodeLocation(node), { variableName: node.name });
|
|
1365
|
-
}
|
|
1366
|
-
if (!isCallable(closure)) {
|
|
1367
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Variable $${node.name} is not a function (got ${typeof closure})`, getNodeLocation(node), { variableName: node.name, actualType: typeof closure });
|
|
1368
|
-
}
|
|
1369
|
-
// Save pipeValue before evaluating arguments
|
|
1370
|
-
const savedPipeValue = ctx.pipeValue;
|
|
1371
|
-
// Evaluate arguments
|
|
1372
|
-
const args = [];
|
|
1373
|
-
for (const arg of node.args) {
|
|
1374
|
-
args.push(await evaluateExpression(arg, ctx));
|
|
1375
|
-
}
|
|
1376
|
-
// Restore pipeValue after argument evaluation
|
|
1377
|
-
ctx.pipeValue = savedPipeValue;
|
|
1378
|
-
// Pipe-style argument passing for script callables:
|
|
1379
|
-
// If no explicit args and we have a pipe input, pass it as first arg
|
|
1380
|
-
// BUT only if:
|
|
1381
|
-
// - The first param has no default (otherwise use the default)
|
|
1382
|
-
// - The pipeInput is not a callable (avoid passing callable to itself after capture)
|
|
1383
|
-
if (isScriptCallable(closure) &&
|
|
1384
|
-
args.length === 0 &&
|
|
1385
|
-
pipeInput !== null &&
|
|
1386
|
-
closure.params.length > 0) {
|
|
1387
|
-
const firstParam = closure.params[0];
|
|
1388
|
-
if (firstParam?.defaultValue === null && !isCallable(pipeInput)) {
|
|
1389
|
-
args.push(pipeInput);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
// Call the callable
|
|
1393
|
-
return invokeCallable(closure, args, ctx, node.span.start);
|
|
1394
|
-
}
|
|
1395
|
-
/**
|
|
1396
|
-
* Invoke any callable (script, runtime, or application) with given arguments.
|
|
1397
|
-
* This is the unified entry point for all callable invocation.
|
|
1398
|
-
*/
|
|
1399
|
-
async function invokeCallable(callable, args, ctx, callLocation) {
|
|
1400
|
-
// Check for abort
|
|
1401
|
-
checkAborted(ctx, undefined);
|
|
1402
|
-
if (callable.kind === 'script') {
|
|
1403
|
-
return invokeScriptCallable(callable, args, ctx, callLocation);
|
|
1404
|
-
}
|
|
1405
|
-
else {
|
|
1406
|
-
// Both 'runtime' and 'application' callables use the same invocation logic
|
|
1407
|
-
return invokeFnCallable(callable, args, ctx, callLocation);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
/** Invoke a function-based callable (runtime or application) */
|
|
1411
|
-
async function invokeFnCallable(callable, args, ctx, callLocation) {
|
|
1412
|
-
// For dict-bound callables, prepend the bound dict if no args provided
|
|
1413
|
-
const effectiveArgs = callable.boundDict && args.length === 0 ? [callable.boundDict] : args;
|
|
1414
|
-
const result = callable.fn(effectiveArgs, ctx, callLocation);
|
|
1415
|
-
return result instanceof Promise ? await result : result;
|
|
1416
|
-
}
|
|
1417
|
-
/** Invoke a script callable (parsed from Rill code) */
|
|
1418
|
-
async function invokeScriptCallable(callable, args, ctx, callLocation) {
|
|
1419
|
-
// Check if first argument is args (unpacking)
|
|
1420
|
-
const firstArg = args[0];
|
|
1421
|
-
if (args.length === 1 && firstArg !== undefined && isArgs(firstArg)) {
|
|
1422
|
-
return invokeScriptCallableWithArgs(callable, firstArg, ctx, callLocation);
|
|
1423
|
-
}
|
|
1424
|
-
// Create a new context with callable's captured vars + params bound to args
|
|
1425
|
-
const callableCtx = {
|
|
1426
|
-
...ctx,
|
|
1427
|
-
variables: new Map(callable.capturedVars),
|
|
1428
|
-
variableTypes: new Map(ctx.variableTypes),
|
|
1429
|
-
};
|
|
1430
|
-
// For dict callables, set $ to the containing dict (late-bound this)
|
|
1431
|
-
if (callable.boundDict) {
|
|
1432
|
-
callableCtx.pipeValue = callable.boundDict;
|
|
1433
|
-
}
|
|
1434
|
-
// Bind parameters to arguments (with defaults and type checking)
|
|
1435
|
-
for (let i = 0; i < callable.params.length; i++) {
|
|
1436
|
-
const param = callable.params[i];
|
|
1437
|
-
let value;
|
|
1438
|
-
if (i < args.length) {
|
|
1439
|
-
// Argument provided
|
|
1440
|
-
value = args[i];
|
|
1441
|
-
}
|
|
1442
|
-
else if (param.defaultValue !== null) {
|
|
1443
|
-
// Use default value
|
|
1444
|
-
value = param.defaultValue;
|
|
1445
|
-
}
|
|
1446
|
-
else {
|
|
1447
|
-
// No arg, no default - strict mode: error
|
|
1448
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Missing argument for parameter '${param.name}' at position ${i}`, callLocation, { paramName: param.name, position: i });
|
|
1449
|
-
}
|
|
1450
|
-
// Validate parameter type (explicit or inferred from default)
|
|
1451
|
-
const expectedType = param.typeName ?? inferTypeFromDefault(param.defaultValue);
|
|
1452
|
-
if (expectedType !== null) {
|
|
1453
|
-
const valueType = inferType(value);
|
|
1454
|
-
if (valueType !== expectedType) {
|
|
1455
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Parameter type mismatch: ${param.name} expects ${expectedType}, got ${valueType}`, callLocation, {
|
|
1456
|
-
paramName: param.name,
|
|
1457
|
-
expectedType,
|
|
1458
|
-
actualType: valueType,
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
callableCtx.variables.set(param.name, value);
|
|
1463
|
-
}
|
|
1464
|
-
// Execute the callable body
|
|
1465
|
-
return evaluateBlockExpression(callable.body, callableCtx);
|
|
1466
|
-
}
|
|
1467
|
-
/** Infer type from a default value (returns null if no default) */
|
|
1468
|
-
function inferTypeFromDefault(defaultValue) {
|
|
1469
|
-
if (defaultValue === null)
|
|
1470
|
-
return null;
|
|
1471
|
-
if (typeof defaultValue === 'string')
|
|
1472
|
-
return 'string';
|
|
1473
|
-
if (typeof defaultValue === 'number')
|
|
1474
|
-
return 'number';
|
|
1475
|
-
if (typeof defaultValue === 'boolean')
|
|
1476
|
-
return 'bool';
|
|
1477
|
-
return null; // Complex types don't infer
|
|
1478
|
-
}
|
|
1479
|
-
/** Invoke a closure with args (unpacked arguments) */
|
|
1480
|
-
async function invokeScriptCallableWithArgs(closure, argsValue, ctx, callLocation) {
|
|
1481
|
-
// Create a new context with closure's captured vars
|
|
1482
|
-
const closureCtx = {
|
|
1483
|
-
...ctx,
|
|
1484
|
-
variables: new Map(closure.capturedVars),
|
|
1485
|
-
variableTypes: new Map(ctx.variableTypes),
|
|
1486
|
-
};
|
|
1487
|
-
// For dict closures, set $ to the containing dict
|
|
1488
|
-
if (closure.boundDict) {
|
|
1489
|
-
closureCtx.pipeValue = closure.boundDict;
|
|
1490
|
-
}
|
|
1491
|
-
// Determine if args are positional (numeric keys) or named (string keys)
|
|
1492
|
-
const hasNumericKeys = [...argsValue.entries.keys()].some((k) => typeof k === 'number');
|
|
1493
|
-
const hasStringKeys = [...argsValue.entries.keys()].some((k) => typeof k === 'string');
|
|
1494
|
-
if (hasNumericKeys && hasStringKeys) {
|
|
1495
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Args cannot mix positional (numeric) and named (string) keys', callLocation);
|
|
1496
|
-
}
|
|
1497
|
-
// Track which params have been bound
|
|
1498
|
-
const boundParams = new Set();
|
|
1499
|
-
if (hasNumericKeys) {
|
|
1500
|
-
// Positional args - bind by position
|
|
1501
|
-
for (const [key, value] of argsValue.entries) {
|
|
1502
|
-
const position = key;
|
|
1503
|
-
const param = closure.params[position];
|
|
1504
|
-
if (param === undefined) {
|
|
1505
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Extra argument at position ${position} (closure has ${closure.params.length} params)`, callLocation, { position, paramCount: closure.params.length });
|
|
1506
|
-
}
|
|
1507
|
-
// Validate type
|
|
1508
|
-
const expectedType = param.typeName ?? inferTypeFromDefault(param.defaultValue);
|
|
1509
|
-
if (expectedType !== null) {
|
|
1510
|
-
const valueType = inferType(value);
|
|
1511
|
-
if (valueType !== expectedType) {
|
|
1512
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Parameter type mismatch: ${param.name} expects ${expectedType}, got ${valueType}`, callLocation, { paramName: param.name, expectedType, actualType: valueType });
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
closureCtx.variables.set(param.name, value);
|
|
1516
|
-
boundParams.add(param.name);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
else if (hasStringKeys) {
|
|
1520
|
-
// Named args - bind by name
|
|
1521
|
-
const paramNames = new Set(closure.params.map((p) => p.name));
|
|
1522
|
-
for (const [key, value] of argsValue.entries) {
|
|
1523
|
-
const name = key;
|
|
1524
|
-
if (!paramNames.has(name)) {
|
|
1525
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown argument '${name}' (valid params: ${[...paramNames].join(', ')})`, callLocation, { argName: name, validParams: [...paramNames] });
|
|
1526
|
-
}
|
|
1527
|
-
const param = closure.params.find((p) => p.name === name);
|
|
1528
|
-
// Validate type
|
|
1529
|
-
const expectedType = param.typeName ?? inferTypeFromDefault(param.defaultValue);
|
|
1530
|
-
if (expectedType !== null) {
|
|
1531
|
-
const valueType = inferType(value);
|
|
1532
|
-
if (valueType !== expectedType) {
|
|
1533
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Parameter type mismatch: ${name} expects ${expectedType}, got ${valueType}`, callLocation, { paramName: name, expectedType, actualType: valueType });
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
closureCtx.variables.set(name, value);
|
|
1537
|
-
boundParams.add(name);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
// Apply defaults for unbound params, error if no default
|
|
1541
|
-
for (const param of closure.params) {
|
|
1542
|
-
if (!boundParams.has(param.name)) {
|
|
1543
|
-
if (param.defaultValue !== null) {
|
|
1544
|
-
closureCtx.variables.set(param.name, param.defaultValue);
|
|
1545
|
-
}
|
|
1546
|
-
else {
|
|
1547
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Missing argument '${param.name}' (no default value)`, callLocation, { paramName: param.name });
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
// Execute the closure body
|
|
1552
|
-
return evaluateBlockExpression(closure.body, closureCtx);
|
|
1553
|
-
}
|
|
1554
|
-
/** Evaluate invoke expression: -> () or -> (args) - invokes pipe value as closure */
|
|
1555
|
-
async function evaluateInvoke(node, input, ctx) {
|
|
1556
|
-
if (!isScriptCallable(input)) {
|
|
1557
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot invoke non-closure value (got ${typeof input})`, getNodeLocation(node));
|
|
1558
|
-
}
|
|
1559
|
-
// Save pipeValue before evaluating arguments
|
|
1560
|
-
const savedPipeValue = ctx.pipeValue;
|
|
1561
|
-
// Evaluate arguments
|
|
1562
|
-
const args = [];
|
|
1563
|
-
for (const arg of node.args) {
|
|
1564
|
-
args.push(await evaluateExpression(arg, ctx));
|
|
1565
|
-
}
|
|
1566
|
-
// Restore pipeValue after argument evaluation
|
|
1567
|
-
ctx.pipeValue = savedPipeValue;
|
|
1568
|
-
// Call the closure
|
|
1569
|
-
return invokeScriptCallable(input, args, ctx, node.span.start);
|
|
1570
|
-
}
|
|
1571
|
-
async function evaluateMethod(node, receiver, ctx) {
|
|
1572
|
-
// Check for abort
|
|
1573
|
-
checkAborted(ctx, node);
|
|
1574
|
-
// Callables don't have methods - must invoke first
|
|
1575
|
-
if (isCallable(receiver)) {
|
|
1576
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Method .${node.name} not available on callable (invoke with -> () first)`, getNodeLocation(node), { methodName: node.name, receiverType: 'callable' });
|
|
1577
|
-
}
|
|
1578
|
-
// Save pipeValue before evaluating arguments
|
|
1579
|
-
const savedPipeValue = ctx.pipeValue;
|
|
1580
|
-
// Evaluate arguments
|
|
1581
|
-
const args = [];
|
|
1582
|
-
for (const arg of node.args) {
|
|
1583
|
-
args.push(await evaluateExpression(arg, ctx));
|
|
1584
|
-
}
|
|
1585
|
-
// Restore pipeValue after argument evaluation
|
|
1586
|
-
ctx.pipeValue = savedPipeValue;
|
|
1587
|
-
// Check if receiver is a dict containing a callable with this name
|
|
1588
|
-
if (isDict(receiver)) {
|
|
1589
|
-
const dictValue = receiver[node.name];
|
|
1590
|
-
if (dictValue !== undefined && isCallable(dictValue)) {
|
|
1591
|
-
return invokeCallable(dictValue, args, ctx, getNodeLocation(node));
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
// Fall back to registered methods
|
|
1595
|
-
const method = ctx.methods.get(node.name);
|
|
1596
|
-
if (!method) {
|
|
1597
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_METHOD, `Unknown method: ${node.name}`, getNodeLocation(node), { methodName: node.name });
|
|
1598
|
-
}
|
|
1599
|
-
const result = method(receiver, args, ctx, getNodeLocation(node));
|
|
1600
|
-
return result instanceof Promise ? await result : result;
|
|
1601
|
-
}
|
|
1602
|
-
// ============================================================
|
|
1603
|
-
// CONTROL FLOW EVALUATION
|
|
1604
|
-
// ============================================================
|
|
1605
|
-
async function evaluateConditional(node, ctx) {
|
|
1606
|
-
// Evaluate condition
|
|
1607
|
-
let conditionResult;
|
|
1608
|
-
if (node.condition) {
|
|
1609
|
-
conditionResult = await evaluateBoolExpr(node.condition, ctx);
|
|
1610
|
-
}
|
|
1611
|
-
else {
|
|
1612
|
-
// No condition means truthy check on pipe value
|
|
1613
|
-
conditionResult = isTruthy(ctx.pipeValue);
|
|
1614
|
-
}
|
|
1615
|
-
if (conditionResult) {
|
|
1616
|
-
return evaluateBlock(node.thenBlock, ctx);
|
|
1617
|
-
}
|
|
1618
|
-
else if (node.elseClause) {
|
|
1619
|
-
if (node.elseClause.type === 'Conditional') {
|
|
1620
|
-
return evaluateConditional(node.elseClause, ctx);
|
|
1621
|
-
}
|
|
1622
|
-
return evaluateBlock(node.elseClause, ctx);
|
|
1623
|
-
}
|
|
1624
|
-
return ctx.pipeValue;
|
|
1625
|
-
}
|
|
1626
|
-
async function evaluateWhileLoop(node, ctx) {
|
|
1627
|
-
// Save pipeValue before evaluating options
|
|
1628
|
-
const inputValue = ctx.pipeValue;
|
|
1629
|
-
// Get max iterations
|
|
1630
|
-
let maxIterations = Infinity;
|
|
1631
|
-
if (node.maxIterations) {
|
|
1632
|
-
const maxVal = await evaluateExpression(node.maxIterations, ctx);
|
|
1633
|
-
if (typeof maxVal === 'number') {
|
|
1634
|
-
maxIterations = maxVal;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
// Restore pipeValue for loop execution
|
|
1638
|
-
ctx.pipeValue = inputValue;
|
|
1639
|
-
let iterations = 0;
|
|
1640
|
-
let value = ctx.pipeValue;
|
|
1641
|
-
try {
|
|
1642
|
-
while (iterations < maxIterations) {
|
|
1643
|
-
// Check for abort at start of each iteration
|
|
1644
|
-
checkAborted(ctx, node);
|
|
1645
|
-
// Check condition
|
|
1646
|
-
const conditionResult = await evaluateBoolExpr(node.condition, ctx);
|
|
1647
|
-
if (!conditionResult)
|
|
1648
|
-
break;
|
|
1649
|
-
// Execute body
|
|
1650
|
-
value = await evaluateBlock(node.body, ctx);
|
|
1651
|
-
ctx.pipeValue = value;
|
|
1652
|
-
iterations++;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
catch (e) {
|
|
1656
|
-
if (e instanceof BreakSignal) {
|
|
1657
|
-
return e.value;
|
|
1658
|
-
}
|
|
1659
|
-
throw e;
|
|
1660
|
-
}
|
|
1661
|
-
return value;
|
|
1662
|
-
}
|
|
1663
|
-
async function evaluateForLoop(node, ctx) {
|
|
1664
|
-
const input = ctx.pipeValue;
|
|
1665
|
-
const results = [];
|
|
1666
|
-
try {
|
|
1667
|
-
if (Array.isArray(input)) {
|
|
1668
|
-
for (const item of input) {
|
|
1669
|
-
// Check for abort at start of each iteration
|
|
1670
|
-
checkAborted(ctx, node);
|
|
1671
|
-
ctx.pipeValue = item;
|
|
1672
|
-
results.push(await evaluateBlock(node.body, ctx));
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
else if (typeof input === 'string') {
|
|
1676
|
-
for (const char of input) {
|
|
1677
|
-
// Check for abort at start of each iteration
|
|
1678
|
-
checkAborted(ctx, node);
|
|
1679
|
-
ctx.pipeValue = char;
|
|
1680
|
-
results.push(await evaluateBlock(node.body, ctx));
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
else {
|
|
1684
|
-
// Single value - execute once
|
|
1685
|
-
checkAborted(ctx, node);
|
|
1686
|
-
results.push(await evaluateBlock(node.body, ctx));
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
catch (e) {
|
|
1690
|
-
if (e instanceof BreakSignal) {
|
|
1691
|
-
return e.value;
|
|
1692
|
-
}
|
|
1693
|
-
throw e;
|
|
1694
|
-
}
|
|
1695
|
-
return results;
|
|
1696
|
-
}
|
|
1697
|
-
async function evaluateBlock(node, ctx) {
|
|
1698
|
-
let lastValue = ctx.pipeValue;
|
|
1699
|
-
for (const stmt of node.statements) {
|
|
1700
|
-
lastValue = await executeStatement(stmt, ctx);
|
|
1701
|
-
}
|
|
1702
|
-
return lastValue;
|
|
1703
|
-
}
|
|
1704
|
-
/** Evaluate a block as an expression, catching ReturnSignal */
|
|
1705
|
-
async function evaluateBlockExpression(node, ctx) {
|
|
1706
|
-
try {
|
|
1707
|
-
return await evaluateBlock(node, ctx);
|
|
1708
|
-
}
|
|
1709
|
-
catch (e) {
|
|
1710
|
-
if (e instanceof ReturnSignal) {
|
|
1711
|
-
return e.value;
|
|
1712
|
-
}
|
|
1713
|
-
throw e;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
// ============================================================
|
|
1717
|
-
// BOOLEAN EXPRESSION EVALUATION
|
|
1718
|
-
// ============================================================
|
|
1719
|
-
async function evaluateBoolExpr(expr, ctx) {
|
|
1720
|
-
if (expr.type === 'Comparison') {
|
|
1721
|
-
return evaluateComparison(expr, ctx);
|
|
1722
|
-
}
|
|
1723
|
-
// expr.type === 'BoolExpr'
|
|
1724
|
-
switch (expr.op) {
|
|
1725
|
-
case 'or': {
|
|
1726
|
-
for (const operand of expr.operands) {
|
|
1727
|
-
if (await evaluateBoolExpr(operand, ctx))
|
|
1728
|
-
return true;
|
|
1729
|
-
}
|
|
1730
|
-
return false;
|
|
1731
|
-
}
|
|
1732
|
-
case 'and': {
|
|
1733
|
-
for (const operand of expr.operands) {
|
|
1734
|
-
if (!(await evaluateBoolExpr(operand, ctx)))
|
|
1735
|
-
return false;
|
|
1736
|
-
}
|
|
1737
|
-
return true;
|
|
1738
|
-
}
|
|
1739
|
-
case 'not': {
|
|
1740
|
-
return !(await evaluateBoolExpr(expr.operand, ctx));
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
async function evaluateComparison(node, ctx) {
|
|
1745
|
-
const left = await evaluateSimplePrimary(node.left, ctx);
|
|
1746
|
-
// No operator means truthy check
|
|
1747
|
-
if (!node.op || !node.right) {
|
|
1748
|
-
return isTruthy(left);
|
|
1749
|
-
}
|
|
1750
|
-
const right = await evaluateSimplePrimary(node.right, ctx);
|
|
1751
|
-
switch (node.op) {
|
|
1752
|
-
case '==':
|
|
1753
|
-
return deepEquals(left, right);
|
|
1754
|
-
case '!=':
|
|
1755
|
-
return !deepEquals(left, right);
|
|
1756
|
-
case '<':
|
|
1757
|
-
return left < right;
|
|
1758
|
-
case '>':
|
|
1759
|
-
return left > right;
|
|
1760
|
-
case '<=':
|
|
1761
|
-
return left <= right;
|
|
1762
|
-
case '>=':
|
|
1763
|
-
return left >= right;
|
|
1764
|
-
default:
|
|
1765
|
-
return false;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
async function evaluateSimplePrimary(node, ctx) {
|
|
1769
|
-
switch (node.type) {
|
|
1770
|
-
case 'StringLiteral':
|
|
1771
|
-
return evaluateString(node, ctx);
|
|
1772
|
-
case 'NumberLiteral':
|
|
1773
|
-
return node.value;
|
|
1774
|
-
case 'BoolLiteral':
|
|
1775
|
-
return node.value;
|
|
1776
|
-
case 'Tuple':
|
|
1777
|
-
return evaluateTuple(node, ctx);
|
|
1778
|
-
case 'Dict':
|
|
1779
|
-
return evaluateDict(node, ctx);
|
|
1780
|
-
case 'Variable':
|
|
1781
|
-
return evaluateVariable(node, ctx);
|
|
1782
|
-
case 'FunctionCall':
|
|
1783
|
-
return evaluateFunctionCall(node, ctx);
|
|
1784
|
-
case 'MethodCall':
|
|
1785
|
-
return evaluateMethod(node, ctx.pipeValue, ctx);
|
|
1786
|
-
case 'Block':
|
|
1787
|
-
return evaluateBlockExpression(node, ctx);
|
|
1788
|
-
case 'Arithmetic':
|
|
1789
|
-
return evaluateArithmetic(node, ctx);
|
|
1790
|
-
default:
|
|
1791
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown simple primary type: ${node.type}`, getNodeLocation(node));
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
// ============================================================
|
|
1795
|
-
// ARITHMETIC
|
|
1796
|
-
// ============================================================
|
|
1797
|
-
/**
|
|
1798
|
-
* Evaluate arithmetic expression: | expr |
|
|
1799
|
-
* No implicit type conversion - operands must be numbers.
|
|
1800
|
-
*/
|
|
1801
|
-
function evaluateArithmetic(node, ctx) {
|
|
1802
|
-
// Single operand (no operator)
|
|
1803
|
-
if (node.op === null) {
|
|
1804
|
-
return evaluateArithOperand(node.left, ctx, node);
|
|
1805
|
-
}
|
|
1806
|
-
// Binary operation
|
|
1807
|
-
const left = evaluateArithOperand(node.left, ctx, node);
|
|
1808
|
-
const right = evaluateArithOperand(node.right, ctx, node);
|
|
1809
|
-
switch (node.op) {
|
|
1810
|
-
case '+':
|
|
1811
|
-
return left + right;
|
|
1812
|
-
case '-':
|
|
1813
|
-
return left - right;
|
|
1814
|
-
case '*':
|
|
1815
|
-
return left * right;
|
|
1816
|
-
case '/':
|
|
1817
|
-
if (right === 0) {
|
|
1818
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Division by zero', node.span.start);
|
|
1819
|
-
}
|
|
1820
|
-
return left / right;
|
|
1821
|
-
case '%':
|
|
1822
|
-
if (right === 0) {
|
|
1823
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Modulo by zero', node.span.start);
|
|
1824
|
-
}
|
|
1825
|
-
return left % right;
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
function evaluateArithOperand(operand, ctx, parent) {
|
|
1829
|
-
switch (operand.type) {
|
|
1830
|
-
case 'NumberLiteral':
|
|
1831
|
-
return operand.value;
|
|
1832
|
-
case 'Variable': {
|
|
1833
|
-
const value = evaluateVariable(operand, ctx);
|
|
1834
|
-
if (typeof value !== 'number') {
|
|
1835
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Arithmetic requires number, got ${inferType(value)}`, operand.span.start);
|
|
1836
|
-
}
|
|
1837
|
-
return value;
|
|
1838
|
-
}
|
|
1839
|
-
case 'Arithmetic':
|
|
1840
|
-
return evaluateArithmetic(operand, ctx);
|
|
1841
|
-
default:
|
|
1842
|
-
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Invalid arithmetic operand: ${operand.type}`, parent.span.start);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
// ============================================================
|
|
1846
|
-
// UTILITY FUNCTIONS
|
|
1847
|
-
// ============================================================
|
|
1848
|
-
function formatValue(value) {
|
|
1849
|
-
if (value === null)
|
|
1850
|
-
return '';
|
|
1851
|
-
if (typeof value === 'string')
|
|
1852
|
-
return value;
|
|
1853
|
-
if (typeof value === 'number')
|
|
1854
|
-
return String(value);
|
|
1855
|
-
if (typeof value === 'boolean')
|
|
1856
|
-
return value ? 'true' : 'false';
|
|
1857
|
-
if (isArgs(value)) {
|
|
1858
|
-
// Format args as *[entries...]
|
|
1859
|
-
const parts = [];
|
|
1860
|
-
for (const [key, val] of value.entries) {
|
|
1861
|
-
if (typeof key === 'number') {
|
|
1862
|
-
parts.push(formatValue(val));
|
|
1863
|
-
}
|
|
1864
|
-
else {
|
|
1865
|
-
parts.push(`${key}: ${formatValue(val)}`);
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
return `*[${parts.join(', ')}]`;
|
|
1869
|
-
}
|
|
1870
|
-
if (isScriptCallable(value)) {
|
|
1871
|
-
const paramStr = value.params.map((p) => p.name).join(', ');
|
|
1872
|
-
return `(${paramStr}) { ... }`;
|
|
1873
|
-
}
|
|
1874
|
-
if (Array.isArray(value))
|
|
1875
|
-
return JSON.stringify(value);
|
|
1876
|
-
return JSON.stringify(value);
|
|
1877
|
-
}
|
|
1878
|
-
/**
|
|
1879
|
-
* Deep structural equality for all Rill values.
|
|
1880
|
-
* - Primitives: value equality
|
|
1881
|
-
* - Tuples: length + recursive element equality
|
|
1882
|
-
* - Dicts: same keys + recursive value equality (order-independent)
|
|
1883
|
-
* - Closures: same params + same body source location (structural identity)
|
|
1884
|
-
*/
|
|
1885
|
-
function deepEquals(a, b) {
|
|
1886
|
-
// Handle primitives and null
|
|
1887
|
-
if (a === b)
|
|
1888
|
-
return true;
|
|
1889
|
-
if (a === null || b === null)
|
|
1890
|
-
return false;
|
|
1891
|
-
if (typeof a !== typeof b)
|
|
1892
|
-
return false;
|
|
1893
|
-
// Primitives (string, number, boolean) - covered by === above
|
|
1894
|
-
if (typeof a !== 'object')
|
|
1895
|
-
return false;
|
|
1896
|
-
// Both are objects at this point
|
|
1897
|
-
// Check for args
|
|
1898
|
-
const aIsArgs = isArgs(a);
|
|
1899
|
-
const bIsArgs = isArgs(b);
|
|
1900
|
-
if (aIsArgs !== bIsArgs)
|
|
1901
|
-
return false;
|
|
1902
|
-
if (aIsArgs && bIsArgs) {
|
|
1903
|
-
if (a.entries.size !== b.entries.size)
|
|
1904
|
-
return false;
|
|
1905
|
-
for (const [key, aVal] of a.entries) {
|
|
1906
|
-
const bVal = b.entries.get(key);
|
|
1907
|
-
if (bVal === undefined || !deepEquals(aVal, bVal))
|
|
1908
|
-
return false;
|
|
1909
|
-
}
|
|
1910
|
-
return true;
|
|
1911
|
-
}
|
|
1912
|
-
// Check for closures
|
|
1913
|
-
const aIsClosure = isScriptCallable(a);
|
|
1914
|
-
const bIsClosure = isScriptCallable(b);
|
|
1915
|
-
if (aIsClosure !== bIsClosure)
|
|
1916
|
-
return false;
|
|
1917
|
-
if (aIsClosure && bIsClosure) {
|
|
1918
|
-
// Closures are equal if they have the same structure
|
|
1919
|
-
// Compare params (name, type, default)
|
|
1920
|
-
if (a.params.length !== b.params.length)
|
|
1921
|
-
return false;
|
|
1922
|
-
for (let i = 0; i < a.params.length; i++) {
|
|
1923
|
-
const ap = a.params[i];
|
|
1924
|
-
const bp = b.params[i];
|
|
1925
|
-
if (ap === undefined || bp === undefined)
|
|
1926
|
-
return false;
|
|
1927
|
-
if (ap.name !== bp.name)
|
|
1928
|
-
return false;
|
|
1929
|
-
if (ap.typeName !== bp.typeName)
|
|
1930
|
-
return false;
|
|
1931
|
-
if (!deepEquals(ap.defaultValue, bp.defaultValue))
|
|
1932
|
-
return false;
|
|
1933
|
-
}
|
|
1934
|
-
// Compare body by source location (same code = same closure)
|
|
1935
|
-
if (a.body.span.start.line !== b.body.span.start.line ||
|
|
1936
|
-
a.body.span.start.column !== b.body.span.start.column) {
|
|
1937
|
-
return false;
|
|
1938
|
-
}
|
|
1939
|
-
// Compare captured variables
|
|
1940
|
-
if (a.capturedVars.size !== b.capturedVars.size)
|
|
1941
|
-
return false;
|
|
1942
|
-
for (const [key, aVal] of a.capturedVars) {
|
|
1943
|
-
const bVal = b.capturedVars.get(key);
|
|
1944
|
-
if (bVal === undefined || !deepEquals(aVal, bVal))
|
|
1945
|
-
return false;
|
|
1946
|
-
}
|
|
1947
|
-
return true;
|
|
1948
|
-
}
|
|
1949
|
-
// Check for arrays (tuples)
|
|
1950
|
-
const aIsArray = Array.isArray(a);
|
|
1951
|
-
const bIsArray = Array.isArray(b);
|
|
1952
|
-
if (aIsArray !== bIsArray)
|
|
1953
|
-
return false;
|
|
1954
|
-
if (aIsArray && bIsArray) {
|
|
1955
|
-
if (a.length !== b.length)
|
|
1956
|
-
return false;
|
|
1957
|
-
for (let i = 0; i < a.length; i++) {
|
|
1958
|
-
const aElem = a[i];
|
|
1959
|
-
const bElem = b[i];
|
|
1960
|
-
if (aElem === undefined || bElem === undefined) {
|
|
1961
|
-
if (aElem !== bElem)
|
|
1962
|
-
return false;
|
|
1963
|
-
}
|
|
1964
|
-
else if (!deepEquals(aElem, bElem)) {
|
|
1965
|
-
return false;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
return true;
|
|
1969
|
-
}
|
|
1970
|
-
// Both are dicts (plain objects)
|
|
1971
|
-
const aKeys = Object.keys(a);
|
|
1972
|
-
const bKeys = Object.keys(b);
|
|
1973
|
-
if (aKeys.length !== bKeys.length)
|
|
1974
|
-
return false;
|
|
1975
|
-
const aDict = a;
|
|
1976
|
-
const bDict = b;
|
|
1977
|
-
for (const key of aKeys) {
|
|
1978
|
-
if (!(key in bDict))
|
|
1979
|
-
return false;
|
|
1980
|
-
const aVal = aDict[key];
|
|
1981
|
-
const bVal = bDict[key];
|
|
1982
|
-
if (aVal === undefined || bVal === undefined) {
|
|
1983
|
-
if (aVal !== bVal)
|
|
1984
|
-
return false;
|
|
1985
|
-
}
|
|
1986
|
-
else if (!deepEquals(aVal, bVal)) {
|
|
1987
|
-
return false;
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
return true;
|
|
1991
|
-
}
|
|
1992
|
-
function isTruthy(value) {
|
|
1993
|
-
if (value === null)
|
|
1994
|
-
return false;
|
|
1995
|
-
if (typeof value === 'boolean')
|
|
1996
|
-
return value;
|
|
1997
|
-
if (typeof value === 'number')
|
|
1998
|
-
return value !== 0;
|
|
1999
|
-
if (typeof value === 'string')
|
|
2000
|
-
return value.length > 0;
|
|
2001
|
-
if (isArgs(value))
|
|
2002
|
-
return value.entries.size > 0;
|
|
2003
|
-
if (isScriptCallable(value))
|
|
2004
|
-
return true;
|
|
2005
|
-
if (Array.isArray(value))
|
|
2006
|
-
return value.length > 0;
|
|
2007
|
-
if (typeof value === 'object')
|
|
2008
|
-
return Object.keys(value).length > 0;
|
|
2009
|
-
return true;
|
|
2010
|
-
}
|
|
2011
|
-
function isEmpty(value) {
|
|
2012
|
-
return !isTruthy(value);
|
|
2013
|
-
}
|
|
2014
|
-
//# sourceMappingURL=runtime.js.map
|