@rcrsr/rill 0.1.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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +1 -0
- package/dist/demo.d.ts +6 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +121 -0
- package/dist/demo.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/errors.d.ts +9 -0
- package/dist/lexer/errors.d.ts.map +1 -0
- package/dist/lexer/errors.js +12 -0
- package/dist/lexer/errors.js.map +1 -0
- package/dist/lexer/helpers.d.ts +14 -0
- package/dist/lexer/helpers.d.ts.map +1 -0
- package/dist/lexer/helpers.js +30 -0
- package/dist/lexer/helpers.js.map +1 -0
- package/dist/lexer/index.d.ts +8 -0
- package/dist/lexer/index.d.ts.map +1 -0
- package/dist/lexer/index.js +8 -0
- package/dist/lexer/index.js.map +1 -0
- package/dist/lexer/operators.d.ts +11 -0
- package/dist/lexer/operators.d.ts.map +1 -0
- package/dist/lexer/operators.js +58 -0
- package/dist/lexer/operators.js.map +1 -0
- package/dist/lexer/readers.d.ts +12 -0
- package/dist/lexer/readers.d.ts.map +1 -0
- package/dist/lexer/readers.js +144 -0
- package/dist/lexer/readers.js.map +1 -0
- package/dist/lexer/state.d.ts +18 -0
- package/dist/lexer/state.d.ts.map +1 -0
- package/dist/lexer/state.js +37 -0
- package/dist/lexer/state.js.map +1 -0
- package/dist/lexer/tokenizer.d.ts +9 -0
- package/dist/lexer/tokenizer.d.ts.map +1 -0
- package/dist/lexer/tokenizer.js +100 -0
- package/dist/lexer/tokenizer.js.map +1 -0
- package/dist/lexer.d.ts +19 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +344 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser/arithmetic.d.ts +16 -0
- package/dist/parser/arithmetic.d.ts.map +1 -0
- package/dist/parser/arithmetic.js +128 -0
- package/dist/parser/arithmetic.js.map +1 -0
- package/dist/parser/boolean.d.ts +15 -0
- package/dist/parser/boolean.d.ts.map +1 -0
- package/dist/parser/boolean.js +20 -0
- package/dist/parser/boolean.js.map +1 -0
- package/dist/parser/control-flow.d.ts +56 -0
- package/dist/parser/control-flow.d.ts.map +1 -0
- package/dist/parser/control-flow.js +167 -0
- package/dist/parser/control-flow.js.map +1 -0
- package/dist/parser/expressions.d.ts +23 -0
- package/dist/parser/expressions.d.ts.map +1 -0
- package/dist/parser/expressions.js +950 -0
- package/dist/parser/expressions.js.map +1 -0
- package/dist/parser/extraction.d.ts +48 -0
- package/dist/parser/extraction.d.ts.map +1 -0
- package/dist/parser/extraction.js +279 -0
- package/dist/parser/extraction.js.map +1 -0
- package/dist/parser/functions.d.ts +20 -0
- package/dist/parser/functions.d.ts.map +1 -0
- package/dist/parser/functions.js +96 -0
- package/dist/parser/functions.js.map +1 -0
- package/dist/parser/helpers.d.ts +94 -0
- package/dist/parser/helpers.d.ts.map +1 -0
- package/dist/parser/helpers.js +225 -0
- package/dist/parser/helpers.js.map +1 -0
- package/dist/parser/index.d.ts +49 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +73 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/literals.d.ts +37 -0
- package/dist/parser/literals.d.ts.map +1 -0
- package/dist/parser/literals.js +373 -0
- package/dist/parser/literals.js.map +1 -0
- package/dist/parser/parser-collect.d.ts +16 -0
- package/dist/parser/parser-collect.d.ts.map +1 -0
- package/dist/parser/parser-collect.js +125 -0
- package/dist/parser/parser-collect.js.map +1 -0
- package/dist/parser/parser-control.d.ts +20 -0
- package/dist/parser/parser-control.d.ts.map +1 -0
- package/dist/parser/parser-control.js +120 -0
- package/dist/parser/parser-control.js.map +1 -0
- package/dist/parser/parser-expr.d.ts +37 -0
- package/dist/parser/parser-expr.d.ts.map +1 -0
- package/dist/parser/parser-expr.js +639 -0
- package/dist/parser/parser-expr.js.map +1 -0
- package/dist/parser/parser-extract.d.ts +17 -0
- package/dist/parser/parser-extract.d.ts.map +1 -0
- package/dist/parser/parser-extract.js +222 -0
- package/dist/parser/parser-extract.js.map +1 -0
- package/dist/parser/parser-functions.d.ts +21 -0
- package/dist/parser/parser-functions.d.ts.map +1 -0
- package/dist/parser/parser-functions.js +155 -0
- package/dist/parser/parser-functions.js.map +1 -0
- package/dist/parser/parser-literals.d.ts +22 -0
- package/dist/parser/parser-literals.d.ts.map +1 -0
- package/dist/parser/parser-literals.js +288 -0
- package/dist/parser/parser-literals.js.map +1 -0
- package/dist/parser/parser-script.d.ts +21 -0
- package/dist/parser/parser-script.d.ts.map +1 -0
- package/dist/parser/parser-script.js +174 -0
- package/dist/parser/parser-script.js.map +1 -0
- package/dist/parser/parser-variables.d.ts +20 -0
- package/dist/parser/parser-variables.d.ts.map +1 -0
- package/dist/parser/parser-variables.js +146 -0
- package/dist/parser/parser-variables.js.map +1 -0
- package/dist/parser/parser.d.ts +49 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +54 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/parser/script.d.ts +14 -0
- package/dist/parser/script.d.ts.map +1 -0
- package/dist/parser/script.js +196 -0
- package/dist/parser/script.js.map +1 -0
- package/dist/parser/state.d.ts +40 -0
- package/dist/parser/state.d.ts.map +1 -0
- package/dist/parser/state.js +129 -0
- package/dist/parser/state.js.map +1 -0
- package/dist/parser/variables.d.ts +10 -0
- package/dist/parser/variables.d.ts.map +1 -0
- package/dist/parser/variables.js +215 -0
- package/dist/parser/variables.js.map +1 -0
- package/dist/runtime/ast-equals.d.ts +13 -0
- package/dist/runtime/ast-equals.d.ts.map +1 -0
- package/dist/runtime/ast-equals.js +447 -0
- package/dist/runtime/ast-equals.js.map +1 -0
- package/dist/runtime/builtins.d.ts +13 -0
- package/dist/runtime/builtins.d.ts.map +1 -0
- package/dist/runtime/builtins.js +180 -0
- package/dist/runtime/builtins.js.map +1 -0
- package/dist/runtime/callable.d.ts +88 -0
- package/dist/runtime/callable.d.ts.map +1 -0
- package/dist/runtime/callable.js +98 -0
- package/dist/runtime/callable.js.map +1 -0
- package/dist/runtime/context.d.ts +13 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +73 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/core/callable.d.ts +171 -0
- package/dist/runtime/core/callable.d.ts.map +1 -0
- package/dist/runtime/core/callable.js +246 -0
- package/dist/runtime/core/callable.js.map +1 -0
- package/dist/runtime/core/context.d.ts +29 -0
- package/dist/runtime/core/context.d.ts.map +1 -0
- package/dist/runtime/core/context.js +154 -0
- package/dist/runtime/core/context.js.map +1 -0
- package/dist/runtime/core/equals.d.ts +9 -0
- package/dist/runtime/core/equals.d.ts.map +1 -0
- package/dist/runtime/core/equals.js +381 -0
- package/dist/runtime/core/equals.js.map +1 -0
- package/dist/runtime/core/eval/base.d.ts +65 -0
- package/dist/runtime/core/eval/base.d.ts.map +1 -0
- package/dist/runtime/core/eval/base.js +112 -0
- package/dist/runtime/core/eval/base.js.map +1 -0
- package/dist/runtime/core/eval/evaluator.d.ts +47 -0
- package/dist/runtime/core/eval/evaluator.d.ts.map +1 -0
- package/dist/runtime/core/eval/evaluator.js +73 -0
- package/dist/runtime/core/eval/evaluator.js.map +1 -0
- package/dist/runtime/core/eval/index.d.ts +57 -0
- package/dist/runtime/core/eval/index.d.ts.map +1 -0
- package/dist/runtime/core/eval/index.js +95 -0
- package/dist/runtime/core/eval/index.js.map +1 -0
- package/dist/runtime/core/eval/mixins/annotations.d.ts +19 -0
- package/dist/runtime/core/eval/mixins/annotations.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/annotations.js +146 -0
- package/dist/runtime/core/eval/mixins/annotations.js.map +1 -0
- package/dist/runtime/core/eval/mixins/closures.d.ts +49 -0
- package/dist/runtime/core/eval/mixins/closures.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/closures.js +479 -0
- package/dist/runtime/core/eval/mixins/closures.js.map +1 -0
- package/dist/runtime/core/eval/mixins/collections.d.ts +24 -0
- package/dist/runtime/core/eval/mixins/collections.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/collections.js +466 -0
- package/dist/runtime/core/eval/mixins/collections.js.map +1 -0
- package/dist/runtime/core/eval/mixins/control-flow.d.ts +27 -0
- package/dist/runtime/core/eval/mixins/control-flow.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/control-flow.js +369 -0
- package/dist/runtime/core/eval/mixins/control-flow.js.map +1 -0
- package/dist/runtime/core/eval/mixins/core.d.ts +24 -0
- package/dist/runtime/core/eval/mixins/core.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/core.js +335 -0
- package/dist/runtime/core/eval/mixins/core.js.map +1 -0
- package/dist/runtime/core/eval/mixins/expressions.d.ts +19 -0
- package/dist/runtime/core/eval/mixins/expressions.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/expressions.js +202 -0
- package/dist/runtime/core/eval/mixins/expressions.js.map +1 -0
- package/dist/runtime/core/eval/mixins/extraction.d.ts +10 -0
- package/dist/runtime/core/eval/mixins/extraction.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/extraction.js +250 -0
- package/dist/runtime/core/eval/mixins/extraction.js.map +1 -0
- package/dist/runtime/core/eval/mixins/literals.d.ts +23 -0
- package/dist/runtime/core/eval/mixins/literals.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/literals.js +180 -0
- package/dist/runtime/core/eval/mixins/literals.js.map +1 -0
- package/dist/runtime/core/eval/mixins/types.d.ts +20 -0
- package/dist/runtime/core/eval/mixins/types.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/types.js +109 -0
- package/dist/runtime/core/eval/mixins/types.js.map +1 -0
- package/dist/runtime/core/eval/mixins/variables.d.ts +34 -0
- package/dist/runtime/core/eval/mixins/variables.d.ts.map +1 -0
- package/dist/runtime/core/eval/mixins/variables.js +247 -0
- package/dist/runtime/core/eval/mixins/variables.js.map +1 -0
- package/dist/runtime/core/eval/types.d.ts +41 -0
- package/dist/runtime/core/eval/types.d.ts.map +1 -0
- package/dist/runtime/core/eval/types.js +10 -0
- package/dist/runtime/core/eval/types.js.map +1 -0
- package/dist/runtime/core/evaluate.d.ts +42 -0
- package/dist/runtime/core/evaluate.d.ts.map +1 -0
- package/dist/runtime/core/evaluate.debug.js +1251 -0
- package/dist/runtime/core/evaluate.js +1913 -0
- package/dist/runtime/core/evaluate.js.map +1 -0
- package/dist/runtime/core/execute.d.ts +26 -0
- package/dist/runtime/core/execute.d.ts.map +1 -0
- package/dist/runtime/core/execute.js +177 -0
- package/dist/runtime/core/execute.js.map +1 -0
- package/dist/runtime/core/signals.d.ts +19 -0
- package/dist/runtime/core/signals.d.ts.map +1 -0
- package/dist/runtime/core/signals.js +26 -0
- package/dist/runtime/core/signals.js.map +1 -0
- package/dist/runtime/core/types.d.ts +177 -0
- package/dist/runtime/core/types.d.ts.map +1 -0
- package/dist/runtime/core/types.js +50 -0
- package/dist/runtime/core/types.js.map +1 -0
- package/dist/runtime/core/values.d.ts +66 -0
- package/dist/runtime/core/values.d.ts.map +1 -0
- package/dist/runtime/core/values.js +240 -0
- package/dist/runtime/core/values.js.map +1 -0
- package/dist/runtime/evaluate.d.ts +32 -0
- package/dist/runtime/evaluate.d.ts.map +1 -0
- package/dist/runtime/evaluate.js +1111 -0
- package/dist/runtime/evaluate.js.map +1 -0
- package/dist/runtime/execute.d.ts +26 -0
- package/dist/runtime/execute.d.ts.map +1 -0
- package/dist/runtime/execute.js +121 -0
- package/dist/runtime/execute.js.map +1 -0
- package/dist/runtime/ext/builtins.d.ts +16 -0
- package/dist/runtime/ext/builtins.d.ts.map +1 -0
- package/dist/runtime/ext/builtins.js +528 -0
- package/dist/runtime/ext/builtins.js.map +1 -0
- package/dist/runtime/ext/content-parser.d.ts +83 -0
- package/dist/runtime/ext/content-parser.d.ts.map +1 -0
- package/dist/runtime/ext/content-parser.js +536 -0
- package/dist/runtime/ext/content-parser.js.map +1 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +34 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/signals.d.ts +19 -0
- package/dist/runtime/signals.d.ts.map +1 -0
- package/dist/runtime/signals.js +26 -0
- package/dist/runtime/signals.js.map +1 -0
- package/dist/runtime/types.d.ts +169 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +50 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/runtime/values.d.ts +50 -0
- package/dist/runtime/values.d.ts.map +1 -0
- package/dist/runtime/values.js +209 -0
- package/dist/runtime/values.js.map +1 -0
- package/dist/runtime.d.ts +254 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +2014 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +752 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +189 -0
- package/dist/types.js.map +1 -0
- package/docs/00_INDEX.md +65 -0
- package/docs/01_guide.md +390 -0
- package/docs/02_types.md +399 -0
- package/docs/03_variables.md +314 -0
- package/docs/04_operators.md +551 -0
- package/docs/05_control-flow.md +350 -0
- package/docs/06_closures.md +353 -0
- package/docs/07_collections.md +686 -0
- package/docs/08_iterators.md +330 -0
- package/docs/09_strings.md +205 -0
- package/docs/10_parsing.md +366 -0
- package/docs/11_reference.md +350 -0
- package/docs/12_examples.md +771 -0
- package/docs/13_modules.md +519 -0
- package/docs/14_host-integration.md +826 -0
- package/docs/15_grammar.ebnf +693 -0
- package/docs/16_conventions.md +696 -0
- package/docs/99_llm-reference.txt +300 -0
- package/docs/assets/logo.png +0 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression Evaluation
|
|
3
|
+
*
|
|
4
|
+
* Internal module for AST evaluation. Not part of public API.
|
|
5
|
+
* All evaluation functions are internal implementation details.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
import { AbortError, AutoExceptionError, RILL_ERROR_CODES, RuntimeError, TimeoutError, } from '../../types.js';
|
|
10
|
+
import { isCallable, isDict, isScriptCallable } from './callable.js';
|
|
11
|
+
import { createChildContext, getVariable, hasVariable } from './context.js';
|
|
12
|
+
import { BreakSignal, ReturnSignal } from './signals.js';
|
|
13
|
+
import { checkType, createTupleFromDict, createTupleFromList, deepEquals, formatValue, inferType, isRillIterator, isTuple, isReservedMethod, isTruthy, } from './values.js';
|
|
14
|
+
// ============================================================
|
|
15
|
+
// CONSTANTS
|
|
16
|
+
// ============================================================
|
|
17
|
+
/** Default maximum iterations when no limit annotation is set */
|
|
18
|
+
const DEFAULT_MAX_ITERATIONS = 10000;
|
|
19
|
+
// ============================================================
|
|
20
|
+
// EXPORTED HELPERS (used by execute.ts)
|
|
21
|
+
// ============================================================
|
|
22
|
+
/** Helper to get location from an AST node */
|
|
23
|
+
function getNodeLocation(node) {
|
|
24
|
+
return node?.span.start;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if execution has been aborted via AbortSignal.
|
|
28
|
+
* Throws AbortError if signal is aborted.
|
|
29
|
+
*/
|
|
30
|
+
export function checkAborted(ctx, node) {
|
|
31
|
+
if (ctx.signal?.aborted) {
|
|
32
|
+
throw new AbortError(getNodeLocation(node));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if the current pipe value matches any autoException pattern.
|
|
37
|
+
* Only checks string values. Throws AutoExceptionError on match.
|
|
38
|
+
*/
|
|
39
|
+
export function checkAutoExceptions(value, ctx, node) {
|
|
40
|
+
if (typeof value !== 'string' || ctx.autoExceptions.length === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const pattern of ctx.autoExceptions) {
|
|
44
|
+
if (pattern.test(value)) {
|
|
45
|
+
throw new AutoExceptionError(pattern.source, value, getNodeLocation(node));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Handle statement capture: set variable and fire observability event.
|
|
51
|
+
* Returns capture info if a capture occurred.
|
|
52
|
+
*/
|
|
53
|
+
export function handleCapture(capture, value, ctx) {
|
|
54
|
+
if (!capture)
|
|
55
|
+
return undefined;
|
|
56
|
+
setVariable(ctx, capture.name, value, capture.typeName, capture.span.start);
|
|
57
|
+
const captureInfo = { name: capture.name, value };
|
|
58
|
+
ctx.observability.onCapture?.(captureInfo);
|
|
59
|
+
return captureInfo;
|
|
60
|
+
}
|
|
61
|
+
// ============================================================
|
|
62
|
+
// TYPE ASSERTION HELPERS
|
|
63
|
+
// ============================================================
|
|
64
|
+
/**
|
|
65
|
+
* Assert that a value is of the expected type.
|
|
66
|
+
* Returns the value unchanged if assertion passes, throws on mismatch.
|
|
67
|
+
* Exported for use by type assertion evaluation.
|
|
68
|
+
*/
|
|
69
|
+
export function assertType(value, expected, location) {
|
|
70
|
+
const actual = inferType(value);
|
|
71
|
+
if (actual !== expected) {
|
|
72
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Type assertion failed: expected ${expected}, got ${actual}`, location, { expectedType: expected, actualType: actual });
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
// ============================================================
|
|
77
|
+
// VARIABLE MANAGEMENT
|
|
78
|
+
// ============================================================
|
|
79
|
+
/**
|
|
80
|
+
* Set a variable with type checking.
|
|
81
|
+
* - First assignment locks the type (inferred or explicit)
|
|
82
|
+
* - Subsequent assignments must match the locked type
|
|
83
|
+
* - Explicit type annotation is validated against value type
|
|
84
|
+
* - Cannot shadow outer scope variables (produces error)
|
|
85
|
+
*/
|
|
86
|
+
function setVariable(ctx, name, value, explicitType, location) {
|
|
87
|
+
const valueType = inferType(value);
|
|
88
|
+
// Check explicit type annotation matches value
|
|
89
|
+
if (explicitType !== null && explicitType !== valueType) {
|
|
90
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Type mismatch: cannot assign ${valueType} to $${name}:${explicitType}`, location, { variableName: name, expectedType: explicitType, actualType: valueType });
|
|
91
|
+
}
|
|
92
|
+
// Check if this is a new variable that would reassign an outer scope variable
|
|
93
|
+
// (error: cannot reassign outer scope variables from child scopes)
|
|
94
|
+
if (!ctx.variables.has(name) && ctx.parent && hasVariable(ctx.parent, name)) {
|
|
95
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot reassign outer variable $${name} from child scope`, location, { variableName: name });
|
|
96
|
+
}
|
|
97
|
+
// Check if variable already has a locked type in current scope
|
|
98
|
+
const lockedType = ctx.variableTypes.get(name);
|
|
99
|
+
if (lockedType !== undefined && lockedType !== valueType) {
|
|
100
|
+
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 });
|
|
101
|
+
}
|
|
102
|
+
// Set the variable and lock its type in current scope
|
|
103
|
+
ctx.variables.set(name, value);
|
|
104
|
+
if (!ctx.variableTypes.has(name)) {
|
|
105
|
+
ctx.variableTypes.set(name, explicitType ?? valueType);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ============================================================
|
|
109
|
+
// TIMEOUT WRAPPER
|
|
110
|
+
// ============================================================
|
|
111
|
+
/**
|
|
112
|
+
* Wrap a promise with a timeout. Returns original promise if no timeout configured.
|
|
113
|
+
*/
|
|
114
|
+
function withTimeout(promise, timeoutMs, functionName, node) {
|
|
115
|
+
if (timeoutMs === undefined) {
|
|
116
|
+
return promise;
|
|
117
|
+
}
|
|
118
|
+
return Promise.race([
|
|
119
|
+
promise,
|
|
120
|
+
new Promise((_, reject) => {
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
reject(new TimeoutError(functionName, timeoutMs, getNodeLocation(node)));
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
// ============================================================
|
|
128
|
+
// EXPRESSION EVALUATION
|
|
129
|
+
// ============================================================
|
|
130
|
+
/**
|
|
131
|
+
* Evaluate argument expressions while preserving the current pipeValue.
|
|
132
|
+
*/
|
|
133
|
+
async function evaluateArgs(argExprs, ctx) {
|
|
134
|
+
const savedPipeValue = ctx.pipeValue;
|
|
135
|
+
const args = [];
|
|
136
|
+
for (const arg of argExprs) {
|
|
137
|
+
args.push(await evaluateExpression(arg, ctx));
|
|
138
|
+
}
|
|
139
|
+
ctx.pipeValue = savedPipeValue;
|
|
140
|
+
return args;
|
|
141
|
+
}
|
|
142
|
+
export async function evaluateExpression(expr, ctx) {
|
|
143
|
+
return evaluatePipeChain(expr, ctx);
|
|
144
|
+
}
|
|
145
|
+
async function evaluatePipeChain(chain, ctx) {
|
|
146
|
+
// Save parent's $ - chains don't leak $ modifications to parent scope
|
|
147
|
+
const savedPipeValue = ctx.pipeValue;
|
|
148
|
+
// Evaluate head (can be PostfixExpr, BinaryExpr, or UnaryExpr)
|
|
149
|
+
let value;
|
|
150
|
+
switch (chain.head.type) {
|
|
151
|
+
case 'BinaryExpr':
|
|
152
|
+
value = await evaluateBinaryExpr(chain.head, ctx);
|
|
153
|
+
break;
|
|
154
|
+
case 'UnaryExpr':
|
|
155
|
+
value = await evaluateUnaryExpr(chain.head, ctx);
|
|
156
|
+
break;
|
|
157
|
+
case 'PostfixExpr':
|
|
158
|
+
value = await evaluatePostfixExpr(chain.head, ctx);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
ctx.pipeValue = value; // OK: local to this chain evaluation
|
|
162
|
+
for (const target of chain.pipes) {
|
|
163
|
+
value = await evaluatePipeTarget(target, value, ctx);
|
|
164
|
+
ctx.pipeValue = value; // OK: flows within chain
|
|
165
|
+
}
|
|
166
|
+
// Handle chain terminator (capture, break, return)
|
|
167
|
+
if (chain.terminator) {
|
|
168
|
+
if (chain.terminator.type === 'Break') {
|
|
169
|
+
// Restore parent's $ before throwing (cleanup)
|
|
170
|
+
ctx.pipeValue = savedPipeValue;
|
|
171
|
+
throw new BreakSignal(value);
|
|
172
|
+
}
|
|
173
|
+
if (chain.terminator.type === 'Return') {
|
|
174
|
+
// Restore parent's $ before throwing (cleanup)
|
|
175
|
+
ctx.pipeValue = savedPipeValue;
|
|
176
|
+
throw new ReturnSignal(value);
|
|
177
|
+
}
|
|
178
|
+
// Capture
|
|
179
|
+
handleCapture(chain.terminator, value, ctx);
|
|
180
|
+
}
|
|
181
|
+
// Restore parent's $ - chain result is returned, but $ doesn't leak
|
|
182
|
+
ctx.pipeValue = savedPipeValue;
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
async function evaluatePostfixExpr(expr, ctx) {
|
|
186
|
+
let value = await evaluatePrimary(expr.primary, ctx);
|
|
187
|
+
for (const method of expr.methods) {
|
|
188
|
+
value = await evaluateMethod(method, value, ctx);
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
async function evaluatePrimary(primary, ctx) {
|
|
193
|
+
switch (primary.type) {
|
|
194
|
+
case 'StringLiteral':
|
|
195
|
+
return evaluateString(primary, ctx);
|
|
196
|
+
case 'NumberLiteral':
|
|
197
|
+
return primary.value;
|
|
198
|
+
case 'BoolLiteral':
|
|
199
|
+
return primary.value;
|
|
200
|
+
case 'Tuple':
|
|
201
|
+
return evaluateTuple(primary, ctx);
|
|
202
|
+
case 'Dict':
|
|
203
|
+
return evaluateDict(primary, ctx);
|
|
204
|
+
case 'Closure':
|
|
205
|
+
return await createClosure(primary, ctx);
|
|
206
|
+
case 'Variable':
|
|
207
|
+
return evaluateVariableAsync(primary, ctx);
|
|
208
|
+
case 'HostCall':
|
|
209
|
+
return evaluateHostCall(primary, ctx);
|
|
210
|
+
case 'ClosureCall':
|
|
211
|
+
return evaluateClosureCall(primary, ctx);
|
|
212
|
+
case 'MethodCall':
|
|
213
|
+
if (ctx.pipeValue === null) {
|
|
214
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, 'Undefined variable: $', primary.span?.start, { variable: '$' });
|
|
215
|
+
}
|
|
216
|
+
return evaluateMethod(primary, ctx.pipeValue, ctx);
|
|
217
|
+
case 'Conditional':
|
|
218
|
+
return evaluateConditional(primary, ctx);
|
|
219
|
+
case 'WhileLoop':
|
|
220
|
+
return evaluateWhileLoop(primary, ctx);
|
|
221
|
+
case 'DoWhileLoop':
|
|
222
|
+
return evaluateDoWhileLoop(primary, ctx);
|
|
223
|
+
case 'Block':
|
|
224
|
+
return evaluateBlockExpression(primary, ctx);
|
|
225
|
+
case 'GroupedExpr':
|
|
226
|
+
return evaluateGroupedExpr(primary, ctx);
|
|
227
|
+
case 'Spread':
|
|
228
|
+
return evaluateSpread(primary, ctx);
|
|
229
|
+
case 'TypeAssertion':
|
|
230
|
+
return evaluateTypeAssertionPrimary(primary, ctx);
|
|
231
|
+
case 'TypeCheck':
|
|
232
|
+
return evaluateTypeCheckPrimary(primary, ctx);
|
|
233
|
+
default:
|
|
234
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown primary type: ${primary.type}`, getNodeLocation(primary));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Evaluate postfix type assertion: expr:type
|
|
239
|
+
* The operand is always present (not null) for postfix form.
|
|
240
|
+
*/
|
|
241
|
+
async function evaluateTypeAssertionPrimary(node, ctx) {
|
|
242
|
+
if (!node.operand) {
|
|
243
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Postfix type assertion requires operand', node.span.start);
|
|
244
|
+
}
|
|
245
|
+
const value = await evaluatePostfixExpr(node.operand, ctx);
|
|
246
|
+
return evaluateTypeAssertion(node, value, ctx);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Evaluate postfix type check: expr:?type
|
|
250
|
+
* The operand is always present (not null) for postfix form.
|
|
251
|
+
*/
|
|
252
|
+
async function evaluateTypeCheckPrimary(node, ctx) {
|
|
253
|
+
if (!node.operand) {
|
|
254
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Postfix type check requires operand', node.span.start);
|
|
255
|
+
}
|
|
256
|
+
const value = await evaluatePostfixExpr(node.operand, ctx);
|
|
257
|
+
return evaluateTypeCheck(node, value, ctx);
|
|
258
|
+
}
|
|
259
|
+
async function evaluatePipeTarget(target, input, ctx) {
|
|
260
|
+
ctx.pipeValue = input;
|
|
261
|
+
switch (target.type) {
|
|
262
|
+
case 'Capture':
|
|
263
|
+
return evaluateCapture(target, input, ctx);
|
|
264
|
+
case 'HostCall':
|
|
265
|
+
return evaluateHostCall(target, ctx);
|
|
266
|
+
case 'ClosureCall':
|
|
267
|
+
return evaluateClosureCallWithPipe(target, input, ctx);
|
|
268
|
+
case 'PipeInvoke':
|
|
269
|
+
return evaluatePipeInvoke(target, input, ctx);
|
|
270
|
+
case 'MethodCall':
|
|
271
|
+
return evaluateMethod(target, input, ctx);
|
|
272
|
+
case 'Conditional':
|
|
273
|
+
return evaluateConditional(target, ctx);
|
|
274
|
+
case 'WhileLoop':
|
|
275
|
+
return evaluateWhileLoop(target, ctx);
|
|
276
|
+
case 'DoWhileLoop':
|
|
277
|
+
return evaluateDoWhileLoop(target, ctx);
|
|
278
|
+
case 'Block':
|
|
279
|
+
return evaluateBlockExpression(target, ctx);
|
|
280
|
+
case 'StringLiteral':
|
|
281
|
+
return evaluateString(target, ctx);
|
|
282
|
+
case 'GroupedExpr':
|
|
283
|
+
return evaluateGroupedExpr(target, ctx);
|
|
284
|
+
case 'ClosureChain':
|
|
285
|
+
return evaluateClosureChain(target, input, ctx);
|
|
286
|
+
case 'Destructure':
|
|
287
|
+
return evaluateDestructure(target, input, ctx);
|
|
288
|
+
case 'Slice':
|
|
289
|
+
return evaluateSlice(target, input, ctx);
|
|
290
|
+
case 'Spread':
|
|
291
|
+
return evaluateSpread(target, ctx);
|
|
292
|
+
case 'TypeAssertion':
|
|
293
|
+
return evaluateTypeAssertion(target, input, ctx);
|
|
294
|
+
case 'TypeCheck':
|
|
295
|
+
return evaluateTypeCheck(target, input, ctx);
|
|
296
|
+
case 'EachExpr':
|
|
297
|
+
return evaluateEach(target, input, ctx);
|
|
298
|
+
case 'MapExpr':
|
|
299
|
+
return evaluateMap(target, input, ctx);
|
|
300
|
+
case 'FoldExpr':
|
|
301
|
+
return evaluateFold(target, input, ctx);
|
|
302
|
+
case 'FilterExpr':
|
|
303
|
+
return evaluateFilter(target, input, ctx);
|
|
304
|
+
case 'Variable':
|
|
305
|
+
// $.field is property access on pipe value, not closure invocation
|
|
306
|
+
if (target.isPipeVar && !target.name && target.accessChain.length > 0) {
|
|
307
|
+
return evaluatePipePropertyAccess(target, input, ctx);
|
|
308
|
+
}
|
|
309
|
+
return evaluateVariableInvoke(target, input, ctx);
|
|
310
|
+
case 'PostfixExpr': {
|
|
311
|
+
// Chained methods on pipe value: -> .a.b.c
|
|
312
|
+
let value = input;
|
|
313
|
+
for (const method of target.methods) {
|
|
314
|
+
value = await evaluateMethod(method, value, ctx);
|
|
315
|
+
}
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown pipe target type: ${target.type}`, getNodeLocation(target));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// ============================================================
|
|
323
|
+
// STATEMENT EXECUTION
|
|
324
|
+
// ============================================================
|
|
325
|
+
export async function executeStatement(stmt, ctx) {
|
|
326
|
+
// Handle annotated statements
|
|
327
|
+
if (stmt.type === 'AnnotatedStatement') {
|
|
328
|
+
return executeAnnotatedStatement(stmt, ctx);
|
|
329
|
+
}
|
|
330
|
+
const value = await evaluateExpression(stmt.expression, ctx);
|
|
331
|
+
// Note: Do NOT set ctx.pipeValue = value here.
|
|
332
|
+
// Statements don't propagate $ to siblings. $ flows only via explicit ->.
|
|
333
|
+
checkAutoExceptions(value, ctx, stmt);
|
|
334
|
+
// Terminator handling is now inside PipeChainNode evaluation
|
|
335
|
+
// (evaluatePipeChain handles capture/break/return terminators)
|
|
336
|
+
return value;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Execute an annotated statement.
|
|
340
|
+
* Evaluates annotations, pushes them to the stack, executes the inner statement,
|
|
341
|
+
* and pops the annotations.
|
|
342
|
+
*/
|
|
343
|
+
async function executeAnnotatedStatement(stmt, ctx) {
|
|
344
|
+
// Evaluate annotation arguments to build annotation dict
|
|
345
|
+
const newAnnotations = await evaluateAnnotations(stmt.annotations, ctx);
|
|
346
|
+
// Merge with inherited annotations (inner overrides outer)
|
|
347
|
+
const inherited = ctx.annotationStack.at(-1) ?? {};
|
|
348
|
+
const merged = { ...inherited, ...newAnnotations };
|
|
349
|
+
// Push merged annotations, execute inner statement, pop
|
|
350
|
+
ctx.annotationStack.push(merged);
|
|
351
|
+
try {
|
|
352
|
+
return await executeStatement(stmt.statement, ctx);
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
ctx.annotationStack.pop();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Evaluate annotation arguments to a dict of key-value pairs.
|
|
360
|
+
*/
|
|
361
|
+
async function evaluateAnnotations(annotations, ctx) {
|
|
362
|
+
const result = {};
|
|
363
|
+
for (const arg of annotations) {
|
|
364
|
+
if (arg.type === 'NamedArg') {
|
|
365
|
+
result[arg.name] = await evaluateExpression(arg.value, ctx);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// SpreadArg: spread tuple/dict keys as annotations
|
|
369
|
+
const spreadValue = await evaluateExpression(arg.expression, ctx);
|
|
370
|
+
if (typeof spreadValue === 'object' &&
|
|
371
|
+
spreadValue !== null &&
|
|
372
|
+
!Array.isArray(spreadValue) &&
|
|
373
|
+
!isCallable(spreadValue)) {
|
|
374
|
+
// Dict: spread all key-value pairs
|
|
375
|
+
Object.assign(result, spreadValue);
|
|
376
|
+
}
|
|
377
|
+
else if (Array.isArray(spreadValue)) {
|
|
378
|
+
// Tuple/list: not valid for annotations (need named keys)
|
|
379
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Annotation spread requires dict with named keys, got list', arg.span.start);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Annotation spread requires dict, got ${typeof spreadValue}`, arg.span.start);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get the current value of an annotation from the annotation stack.
|
|
390
|
+
*/
|
|
391
|
+
export function getAnnotation(ctx, key) {
|
|
392
|
+
return ctx.annotationStack.at(-1)?.[key];
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get the iteration limit for loops from the `limit` annotation.
|
|
396
|
+
* Returns the default if not set or if the value is not a positive number.
|
|
397
|
+
*/
|
|
398
|
+
function getIterationLimit(ctx) {
|
|
399
|
+
const limit = getAnnotation(ctx, 'limit');
|
|
400
|
+
if (typeof limit === 'number' && limit > 0) {
|
|
401
|
+
return Math.floor(limit);
|
|
402
|
+
}
|
|
403
|
+
return DEFAULT_MAX_ITERATIONS;
|
|
404
|
+
}
|
|
405
|
+
async function evaluateClosureChain(node, input, ctx) {
|
|
406
|
+
const target = await evaluateExpression(node.target, ctx);
|
|
407
|
+
const closures = Array.isArray(target) ? target : [target];
|
|
408
|
+
let accumulated = input;
|
|
409
|
+
for (const closure of closures) {
|
|
410
|
+
accumulated = await invokeAsCallableOrFunction(closure, [accumulated], ctx, node.span.start);
|
|
411
|
+
}
|
|
412
|
+
return accumulated;
|
|
413
|
+
}
|
|
414
|
+
// ============================================================
|
|
415
|
+
// COLLECTION OPERATORS (each, map, fold)
|
|
416
|
+
// ============================================================
|
|
417
|
+
/**
|
|
418
|
+
* Get iterable elements from input value.
|
|
419
|
+
* Returns array of elements to iterate over.
|
|
420
|
+
* For dicts, returns array of { key, value } objects.
|
|
421
|
+
*/
|
|
422
|
+
/**
|
|
423
|
+
* Check if a value is a rill iterator (dict with value, done, next fields).
|
|
424
|
+
* Iterators follow the protocol: { value: any, done: bool, next: closure }
|
|
425
|
+
*/
|
|
426
|
+
/**
|
|
427
|
+
* Expand a rill iterator into an array of elements.
|
|
428
|
+
* Respects iteration limits to prevent infinite loops.
|
|
429
|
+
*/
|
|
430
|
+
async function expandIterator(iterator, ctx, node, limit = 10000) {
|
|
431
|
+
const elements = [];
|
|
432
|
+
let current = iterator;
|
|
433
|
+
let count = 0;
|
|
434
|
+
while (!current['done'] && count < limit) {
|
|
435
|
+
checkAborted(ctx, undefined);
|
|
436
|
+
const val = current['value'];
|
|
437
|
+
if (val !== undefined) {
|
|
438
|
+
elements.push(val);
|
|
439
|
+
}
|
|
440
|
+
count++;
|
|
441
|
+
// Invoke next() to get the next iterator
|
|
442
|
+
const nextClosure = current['next'];
|
|
443
|
+
if (nextClosure === undefined || !isCallable(nextClosure)) {
|
|
444
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Iterator .next must be a closure', node.span.start);
|
|
445
|
+
}
|
|
446
|
+
const nextIterator = await invokeCallable(nextClosure, [], ctx, node.span.start);
|
|
447
|
+
if (!isRillIterator(nextIterator)) {
|
|
448
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Iterator .next must return an iterator', node.span.start);
|
|
449
|
+
}
|
|
450
|
+
current = nextIterator;
|
|
451
|
+
}
|
|
452
|
+
if (count >= limit && !current['done']) {
|
|
453
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Iterator exceeded ${limit} elements (use ^(limit: N) to increase)`, node.span.start);
|
|
454
|
+
}
|
|
455
|
+
return elements;
|
|
456
|
+
}
|
|
457
|
+
async function getIterableElements(input, ctx, node) {
|
|
458
|
+
if (Array.isArray(input)) {
|
|
459
|
+
return input;
|
|
460
|
+
}
|
|
461
|
+
if (typeof input === 'string') {
|
|
462
|
+
return [...input];
|
|
463
|
+
}
|
|
464
|
+
// Check for iterator protocol BEFORE generic dict handling
|
|
465
|
+
if (isRillIterator(input)) {
|
|
466
|
+
return expandIterator(input, ctx, node);
|
|
467
|
+
}
|
|
468
|
+
if (isDict(input)) {
|
|
469
|
+
// Dict iteration: sorted keys, each element is { key, value }
|
|
470
|
+
const keys = Object.keys(input).sort();
|
|
471
|
+
return keys.map((key) => ({
|
|
472
|
+
key,
|
|
473
|
+
value: input[key],
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Collection operators require list, string, dict, or iterator, got ${inferType(input)}`, node.span.start);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Evaluate collection body for a single element.
|
|
480
|
+
* Handles all body forms: closure, block, grouped, variable, postfix, spread.
|
|
481
|
+
*/
|
|
482
|
+
async function evaluateIteratorBody(body, element, accumulator, ctx) {
|
|
483
|
+
switch (body.type) {
|
|
484
|
+
case 'Closure': {
|
|
485
|
+
// Inline closure: invoke with element (and accumulator if present in params)
|
|
486
|
+
const closure = await createClosure(body, ctx);
|
|
487
|
+
const args = [element];
|
|
488
|
+
// Accumulator is passed as second arg if closure has 2+ params
|
|
489
|
+
if (accumulator !== null && closure.params.length >= 2) {
|
|
490
|
+
args.push(accumulator);
|
|
491
|
+
}
|
|
492
|
+
// Create context with $@ for closures that use it (e.g., |x| { $x + $@ })
|
|
493
|
+
let invokeCtx = ctx;
|
|
494
|
+
let closureToInvoke = closure;
|
|
495
|
+
if (accumulator !== null) {
|
|
496
|
+
invokeCtx = createChildContext(ctx);
|
|
497
|
+
invokeCtx.variables.set('@', accumulator);
|
|
498
|
+
// Create new closure with updated definingScope to include $@
|
|
499
|
+
closureToInvoke = { ...closure, definingScope: invokeCtx };
|
|
500
|
+
}
|
|
501
|
+
return invokeCallable(closureToInvoke, args, invokeCtx, body.span.start);
|
|
502
|
+
}
|
|
503
|
+
case 'Block': {
|
|
504
|
+
// Block: evaluate with $ = element, $@ = accumulator
|
|
505
|
+
const blockCtx = createChildContext(ctx);
|
|
506
|
+
blockCtx.pipeValue = element;
|
|
507
|
+
if (accumulator !== null) {
|
|
508
|
+
blockCtx.variables.set('@', accumulator);
|
|
509
|
+
}
|
|
510
|
+
return evaluateBlockExpression(body, blockCtx);
|
|
511
|
+
}
|
|
512
|
+
case 'GroupedExpr': {
|
|
513
|
+
// Grouped: evaluate with $ = element, $@ = accumulator (for fold)
|
|
514
|
+
const groupedCtx = createChildContext(ctx);
|
|
515
|
+
groupedCtx.pipeValue = element;
|
|
516
|
+
if (accumulator !== null) {
|
|
517
|
+
groupedCtx.variables.set('@', accumulator);
|
|
518
|
+
}
|
|
519
|
+
return evaluateGroupedExpr(body, groupedCtx);
|
|
520
|
+
}
|
|
521
|
+
case 'Variable': {
|
|
522
|
+
// Bare $ = identity, return element unchanged
|
|
523
|
+
if (body.isPipeVar && !body.name && body.accessChain.length === 0) {
|
|
524
|
+
return element;
|
|
525
|
+
}
|
|
526
|
+
// $[idx] or $.field - evaluate access chain on current element
|
|
527
|
+
if (body.isPipeVar && body.accessChain.length > 0) {
|
|
528
|
+
const bodyCtx = createChildContext(ctx);
|
|
529
|
+
bodyCtx.pipeValue = element;
|
|
530
|
+
return evaluateVariableAsync(body, bodyCtx);
|
|
531
|
+
}
|
|
532
|
+
// Variable closure: get closure and invoke with element
|
|
533
|
+
const varValue = getVariable(ctx, body.name ?? '');
|
|
534
|
+
if (!varValue) {
|
|
535
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, `Undefined variable: $${body.name}`, body.span.start);
|
|
536
|
+
}
|
|
537
|
+
if (!isCallable(varValue)) {
|
|
538
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Collection body variable must be callable, got ${inferType(varValue)}`, body.span.start);
|
|
539
|
+
}
|
|
540
|
+
const args = [element];
|
|
541
|
+
if (accumulator !== null &&
|
|
542
|
+
varValue.kind === 'script' &&
|
|
543
|
+
varValue.params.length >= 2) {
|
|
544
|
+
args.push(accumulator);
|
|
545
|
+
}
|
|
546
|
+
return invokeCallable(varValue, args, ctx, body.span.start);
|
|
547
|
+
}
|
|
548
|
+
case 'PostfixExpr': {
|
|
549
|
+
// PostfixExpr: evaluate with $ = element
|
|
550
|
+
const postfixCtx = createChildContext(ctx);
|
|
551
|
+
postfixCtx.pipeValue = element;
|
|
552
|
+
return evaluatePostfixExpr(body, postfixCtx);
|
|
553
|
+
}
|
|
554
|
+
case 'Spread': {
|
|
555
|
+
// Spread: convert element to tuple
|
|
556
|
+
const spreadCtx = createChildContext(ctx);
|
|
557
|
+
spreadCtx.pipeValue = element;
|
|
558
|
+
return evaluateSpread(body, spreadCtx);
|
|
559
|
+
}
|
|
560
|
+
default:
|
|
561
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown collection body type: ${body.type}`, body.span.start);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Evaluate each expression: sequential iteration returning list of all results.
|
|
566
|
+
*
|
|
567
|
+
* With accumulator: returns list of running values (scan/prefix-sum pattern)
|
|
568
|
+
* Without accumulator: returns list of body results
|
|
569
|
+
*
|
|
570
|
+
* Supports break for early termination.
|
|
571
|
+
*/
|
|
572
|
+
async function evaluateEach(node, input, ctx) {
|
|
573
|
+
const elements = await getIterableElements(input, ctx, node);
|
|
574
|
+
// Empty collection: return []
|
|
575
|
+
if (elements.length === 0) {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
// Get initial accumulator value if present
|
|
579
|
+
let accumulator = null;
|
|
580
|
+
if (node.accumulator) {
|
|
581
|
+
accumulator = await evaluateExpression(node.accumulator, ctx);
|
|
582
|
+
}
|
|
583
|
+
else if (node.body.type === 'Closure' && node.body.params.length >= 2) {
|
|
584
|
+
// Inline closure with accumulator: |x, acc = init| body
|
|
585
|
+
const lastParam = node.body.params[node.body.params.length - 1];
|
|
586
|
+
if (lastParam?.defaultValue) {
|
|
587
|
+
accumulator = await evaluatePrimary(lastParam.defaultValue, ctx);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const results = [];
|
|
591
|
+
try {
|
|
592
|
+
for (const element of elements) {
|
|
593
|
+
checkAborted(ctx, node);
|
|
594
|
+
const result = await evaluateIteratorBody(node.body, element, accumulator, ctx);
|
|
595
|
+
results.push(result);
|
|
596
|
+
// Update accumulator for next iteration (scan pattern)
|
|
597
|
+
if (accumulator !== null) {
|
|
598
|
+
accumulator = result;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
if (e instanceof BreakSignal) {
|
|
604
|
+
// Break: return results collected so far
|
|
605
|
+
return results;
|
|
606
|
+
}
|
|
607
|
+
throw e;
|
|
608
|
+
}
|
|
609
|
+
return results;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Evaluate map expression: parallel iteration returning list of all results.
|
|
613
|
+
*
|
|
614
|
+
* Uses Promise.all for concurrent execution.
|
|
615
|
+
* Concurrency limit via ^(limit: N) annotation.
|
|
616
|
+
*/
|
|
617
|
+
async function evaluateMap(node, input, ctx) {
|
|
618
|
+
const elements = await getIterableElements(input, ctx, node);
|
|
619
|
+
// Empty collection: return []
|
|
620
|
+
if (elements.length === 0) {
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
// Check for concurrency limit annotation
|
|
624
|
+
const limitAnnotation = getAnnotation(ctx, 'limit');
|
|
625
|
+
const concurrencyLimit = typeof limitAnnotation === 'number' && limitAnnotation > 0
|
|
626
|
+
? Math.floor(limitAnnotation)
|
|
627
|
+
: Infinity;
|
|
628
|
+
if (concurrencyLimit === Infinity) {
|
|
629
|
+
// No limit: all in parallel
|
|
630
|
+
const promises = elements.map((element) => evaluateIteratorBody(node.body, element, null, ctx));
|
|
631
|
+
return Promise.all(promises);
|
|
632
|
+
}
|
|
633
|
+
// With limit: process in batches
|
|
634
|
+
const results = [];
|
|
635
|
+
for (let i = 0; i < elements.length; i += concurrencyLimit) {
|
|
636
|
+
checkAborted(ctx, node);
|
|
637
|
+
const batch = elements.slice(i, i + concurrencyLimit);
|
|
638
|
+
const batchPromises = batch.map((element) => evaluateIteratorBody(node.body, element, null, ctx));
|
|
639
|
+
const batchResults = await Promise.all(batchPromises);
|
|
640
|
+
results.push(...batchResults);
|
|
641
|
+
}
|
|
642
|
+
return results;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Evaluate fold expression: sequential reduction returning final result only.
|
|
646
|
+
*
|
|
647
|
+
* Accumulator is required.
|
|
648
|
+
* Empty collection: returns initial accumulator value.
|
|
649
|
+
*/
|
|
650
|
+
async function evaluateFold(node, input, ctx) {
|
|
651
|
+
const elements = await getIterableElements(input, ctx, node);
|
|
652
|
+
// Get initial accumulator value
|
|
653
|
+
let accumulator;
|
|
654
|
+
if (node.accumulator) {
|
|
655
|
+
accumulator = await evaluateExpression(node.accumulator, ctx);
|
|
656
|
+
}
|
|
657
|
+
else if (node.body.type === 'Closure' && node.body.params.length >= 2) {
|
|
658
|
+
// Inline closure with accumulator: |x, acc = init| body
|
|
659
|
+
const lastParam = node.body.params[node.body.params.length - 1];
|
|
660
|
+
if (lastParam?.defaultValue) {
|
|
661
|
+
accumulator = await evaluatePrimary(lastParam.defaultValue, ctx);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Fold requires accumulator: use |x, acc = init| or fold(init) { }', node.span.start);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (node.body.type === 'Variable' && !node.body.isPipeVar) {
|
|
668
|
+
// Variable closure: the closure itself must have an accumulator default
|
|
669
|
+
const varValue = getVariable(ctx, node.body.name ?? '');
|
|
670
|
+
if (!varValue || !isCallable(varValue) || varValue.kind !== 'script') {
|
|
671
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Fold variable must be a script closure with accumulator parameter', node.span.start);
|
|
672
|
+
}
|
|
673
|
+
const lastParam = varValue.params[varValue.params.length - 1];
|
|
674
|
+
if (lastParam && lastParam.defaultValue !== null) {
|
|
675
|
+
accumulator = lastParam.defaultValue;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Fold closure must have accumulator parameter with default value', node.span.start);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Fold requires accumulator: use |x, acc = init| or fold(init) { }', node.span.start);
|
|
683
|
+
}
|
|
684
|
+
// Empty collection: return initial accumulator
|
|
685
|
+
if (elements.length === 0) {
|
|
686
|
+
return accumulator;
|
|
687
|
+
}
|
|
688
|
+
for (const element of elements) {
|
|
689
|
+
checkAborted(ctx, node);
|
|
690
|
+
accumulator = await evaluateIteratorBody(node.body, element, accumulator, ctx);
|
|
691
|
+
}
|
|
692
|
+
return accumulator;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Evaluate filter expression: parallel filtering, returns elements where predicate is truthy.
|
|
696
|
+
*
|
|
697
|
+
* Executes predicate for all elements concurrently.
|
|
698
|
+
* Preserves original element order.
|
|
699
|
+
* Empty collection: returns [].
|
|
700
|
+
*/
|
|
701
|
+
async function evaluateFilter(node, input, ctx) {
|
|
702
|
+
const elements = await getIterableElements(input, ctx, node);
|
|
703
|
+
// Empty collection: return []
|
|
704
|
+
if (elements.length === 0) {
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
// Evaluate predicate for all elements in parallel
|
|
708
|
+
const predicatePromises = elements.map(async (element) => {
|
|
709
|
+
checkAborted(ctx, node);
|
|
710
|
+
const result = await evaluateIteratorBody(node.body, element, null, ctx);
|
|
711
|
+
// Predicate must return boolean
|
|
712
|
+
if (typeof result !== 'boolean') {
|
|
713
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Filter predicate must return boolean, got ${inferType(result)}`, node.span.start);
|
|
714
|
+
}
|
|
715
|
+
return { element, keep: result };
|
|
716
|
+
});
|
|
717
|
+
const results = await Promise.all(predicatePromises);
|
|
718
|
+
// Filter elements where predicate was true
|
|
719
|
+
return results.filter((r) => r.keep).map((r) => r.element);
|
|
720
|
+
}
|
|
721
|
+
async function invokeAsCallableOrFunction(callableOrName, args, ctx, location) {
|
|
722
|
+
if (isCallable(callableOrName)) {
|
|
723
|
+
return invokeCallable(callableOrName, args, ctx, location);
|
|
724
|
+
}
|
|
725
|
+
if (typeof callableOrName === 'string') {
|
|
726
|
+
const fn = ctx.functions.get(callableOrName);
|
|
727
|
+
if (fn) {
|
|
728
|
+
return fn(args, ctx, location);
|
|
729
|
+
}
|
|
730
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_FUNCTION, `Unknown function: ${callableOrName}`, location, { functionName: callableOrName });
|
|
731
|
+
}
|
|
732
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Expected callable or function name, got ${typeof callableOrName}`, location);
|
|
733
|
+
}
|
|
734
|
+
function evaluateCapture(node, input, ctx) {
|
|
735
|
+
setVariable(ctx, node.name, input, node.typeName, node.span.start);
|
|
736
|
+
ctx.observability.onCapture?.({ name: node.name, value: input });
|
|
737
|
+
return input;
|
|
738
|
+
}
|
|
739
|
+
// ============================================================
|
|
740
|
+
// EXTRACTION OPERATORS
|
|
741
|
+
// ============================================================
|
|
742
|
+
function evaluateDestructure(node, input, ctx) {
|
|
743
|
+
const isList = Array.isArray(input);
|
|
744
|
+
const isDictInput = isDict(input);
|
|
745
|
+
const firstNonSkip = node.elements.find((e) => e.kind !== 'skip');
|
|
746
|
+
const isKeyPattern = firstNonSkip?.kind === 'keyValue';
|
|
747
|
+
if (isKeyPattern) {
|
|
748
|
+
if (!isDictInput) {
|
|
749
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Key destructure requires dict, got ${isList ? 'list' : typeof input}`, node.span.start);
|
|
750
|
+
}
|
|
751
|
+
for (const elem of node.elements) {
|
|
752
|
+
if (elem.kind === 'skip')
|
|
753
|
+
continue;
|
|
754
|
+
if (elem.kind === 'nested') {
|
|
755
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Nested destructure not supported in dict patterns', elem.span.start);
|
|
756
|
+
}
|
|
757
|
+
if (elem.kind !== 'keyValue' || elem.key === null || elem.name === null) {
|
|
758
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Dict destructure requires key: $var patterns', elem.span.start);
|
|
759
|
+
}
|
|
760
|
+
const dictInput = input;
|
|
761
|
+
if (!(elem.key in dictInput)) {
|
|
762
|
+
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) });
|
|
763
|
+
}
|
|
764
|
+
const dictValue = dictInput[elem.key];
|
|
765
|
+
if (dictValue === undefined) {
|
|
766
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Key '${elem.key}' has undefined value`, elem.span.start);
|
|
767
|
+
}
|
|
768
|
+
setVariable(ctx, elem.name, dictValue, elem.typeName, elem.span.start);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
if (!isList) {
|
|
773
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Positional destructure requires list, got ${isDictInput ? 'dict' : typeof input}`, node.span.start);
|
|
774
|
+
}
|
|
775
|
+
const listInput = input;
|
|
776
|
+
if (node.elements.length !== listInput.length) {
|
|
777
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Destructure pattern has ${node.elements.length} elements, list has ${listInput.length}`, node.span.start);
|
|
778
|
+
}
|
|
779
|
+
for (let i = 0; i < node.elements.length; i++) {
|
|
780
|
+
const elem = node.elements[i];
|
|
781
|
+
const value = listInput[i];
|
|
782
|
+
if (elem === undefined || value === undefined) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (elem.kind === 'skip')
|
|
786
|
+
continue;
|
|
787
|
+
if (elem.kind === 'nested' && elem.nested) {
|
|
788
|
+
evaluateDestructure(elem.nested, value, ctx);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (elem.name === null) {
|
|
792
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Invalid destructure element', elem.span.start);
|
|
793
|
+
}
|
|
794
|
+
setVariable(ctx, elem.name, value, elem.typeName, elem.span.start);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return input;
|
|
798
|
+
}
|
|
799
|
+
async function evaluateSlice(node, input, ctx) {
|
|
800
|
+
const isList = Array.isArray(input);
|
|
801
|
+
const isString = typeof input === 'string';
|
|
802
|
+
if (!isList && !isString) {
|
|
803
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Slice requires list or string, got ${isDict(input) ? 'dict' : typeof input}`, node.span.start);
|
|
804
|
+
}
|
|
805
|
+
const startBound = node.start
|
|
806
|
+
? await evaluateSliceBound(node.start, ctx)
|
|
807
|
+
: null;
|
|
808
|
+
const stopBound = node.stop ? await evaluateSliceBound(node.stop, ctx) : null;
|
|
809
|
+
const stepBound = node.step ? await evaluateSliceBound(node.step, ctx) : null;
|
|
810
|
+
if (isList) {
|
|
811
|
+
return applySlice(input, input.length, startBound, stopBound, stepBound);
|
|
812
|
+
}
|
|
813
|
+
return applySlice(input, input.length, startBound, stopBound, stepBound);
|
|
814
|
+
}
|
|
815
|
+
async function evaluateSliceBound(bound, ctx) {
|
|
816
|
+
if (bound === null) {
|
|
817
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Slice bound is null', undefined);
|
|
818
|
+
}
|
|
819
|
+
switch (bound.type) {
|
|
820
|
+
case 'NumberLiteral':
|
|
821
|
+
return bound.value;
|
|
822
|
+
case 'Variable': {
|
|
823
|
+
const value = evaluateVariable(bound, ctx);
|
|
824
|
+
if (typeof value !== 'number') {
|
|
825
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Slice bound must be number, got ${typeof value}`, bound.span.start);
|
|
826
|
+
}
|
|
827
|
+
return value;
|
|
828
|
+
}
|
|
829
|
+
case 'GroupedExpr': {
|
|
830
|
+
const value = await evaluateGroupedExpr(bound, ctx);
|
|
831
|
+
if (typeof value !== 'number') {
|
|
832
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Slice bound must be number, got ${typeof value}`, bound.span.start);
|
|
833
|
+
}
|
|
834
|
+
return value;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function applySlice(input, len, start, stop, step) {
|
|
839
|
+
const actualStep = step ?? 1;
|
|
840
|
+
if (actualStep === 0) {
|
|
841
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Slice step cannot be zero', undefined);
|
|
842
|
+
}
|
|
843
|
+
const normalizeIndex = (idx, defaultVal, forStep) => {
|
|
844
|
+
if (idx === null)
|
|
845
|
+
return defaultVal;
|
|
846
|
+
let normalized = idx < 0 ? len + idx : idx;
|
|
847
|
+
if (forStep > 0) {
|
|
848
|
+
normalized = Math.max(0, Math.min(len, normalized));
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
normalized = Math.max(-1, Math.min(len - 1, normalized));
|
|
852
|
+
}
|
|
853
|
+
return normalized;
|
|
854
|
+
};
|
|
855
|
+
const actualStart = normalizeIndex(start, actualStep > 0 ? 0 : len - 1, actualStep);
|
|
856
|
+
const actualStop = normalizeIndex(stop, actualStep > 0 ? len : -1, actualStep);
|
|
857
|
+
const indices = [];
|
|
858
|
+
if (actualStep > 0) {
|
|
859
|
+
for (let i = actualStart; i < actualStop; i += actualStep) {
|
|
860
|
+
indices.push(i);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
for (let i = actualStart; i > actualStop; i += actualStep) {
|
|
865
|
+
indices.push(i);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (Array.isArray(input)) {
|
|
869
|
+
return indices.map((i) => input[i]);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
return indices.map((i) => input[i]).join('');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function evaluateSpread(node, ctx) {
|
|
876
|
+
let value;
|
|
877
|
+
if (node.operand === null) {
|
|
878
|
+
value = ctx.pipeValue;
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
value = await evaluateExpression(node.operand, ctx);
|
|
882
|
+
}
|
|
883
|
+
if (Array.isArray(value)) {
|
|
884
|
+
return createTupleFromList(value);
|
|
885
|
+
}
|
|
886
|
+
if (isDict(value)) {
|
|
887
|
+
return createTupleFromDict(value);
|
|
888
|
+
}
|
|
889
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Spread requires list or dict, got ${inferType(value)}`, node.span.start);
|
|
890
|
+
}
|
|
891
|
+
// ============================================================
|
|
892
|
+
// TYPE OPERATIONS
|
|
893
|
+
// ============================================================
|
|
894
|
+
/**
|
|
895
|
+
* Evaluate type assertion: expr:type or :type (shorthand for $:type)
|
|
896
|
+
* Returns value unchanged if type matches, throws on mismatch.
|
|
897
|
+
*/
|
|
898
|
+
async function evaluateTypeAssertion(node, input, ctx) {
|
|
899
|
+
// If operand is null, use the input (pipe value)
|
|
900
|
+
// Otherwise, evaluate the operand
|
|
901
|
+
const value = node.operand
|
|
902
|
+
? await evaluatePostfixExpr(node.operand, ctx)
|
|
903
|
+
: input;
|
|
904
|
+
return assertType(value, node.typeName, node.span.start);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Evaluate type check: expr:?type or :?type (shorthand for $:?type)
|
|
908
|
+
* Returns true if type matches, false otherwise.
|
|
909
|
+
*/
|
|
910
|
+
async function evaluateTypeCheck(node, input, ctx) {
|
|
911
|
+
// If operand is null, use the input (pipe value)
|
|
912
|
+
// Otherwise, evaluate the operand
|
|
913
|
+
const value = node.operand
|
|
914
|
+
? await evaluatePostfixExpr(node.operand, ctx)
|
|
915
|
+
: input;
|
|
916
|
+
return checkType(value, node.typeName);
|
|
917
|
+
}
|
|
918
|
+
// ============================================================
|
|
919
|
+
// LITERAL EVALUATION
|
|
920
|
+
// ============================================================
|
|
921
|
+
async function evaluateString(node, ctx) {
|
|
922
|
+
let result = '';
|
|
923
|
+
// Save pipeValue since interpolation expressions can modify it
|
|
924
|
+
const savedPipeValue = ctx.pipeValue;
|
|
925
|
+
for (const part of node.parts) {
|
|
926
|
+
if (typeof part === 'string') {
|
|
927
|
+
result += part;
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
// InterpolationNode: evaluate the expression
|
|
931
|
+
// Restore pipeValue before each interpolation so they all see the same value
|
|
932
|
+
ctx.pipeValue = savedPipeValue;
|
|
933
|
+
const value = await evaluateExpression(part.expression, ctx);
|
|
934
|
+
result += formatValue(value);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// Restore pipeValue after string evaluation
|
|
938
|
+
ctx.pipeValue = savedPipeValue;
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
async function evaluateTuple(node, ctx) {
|
|
942
|
+
const elements = [];
|
|
943
|
+
for (const elem of node.elements) {
|
|
944
|
+
elements.push(await evaluateExpression(elem, ctx));
|
|
945
|
+
}
|
|
946
|
+
return elements;
|
|
947
|
+
}
|
|
948
|
+
function isClosureExpr(expr) {
|
|
949
|
+
if (expr.pipes.length > 0)
|
|
950
|
+
return false;
|
|
951
|
+
if (expr.head.type !== 'PostfixExpr')
|
|
952
|
+
return false;
|
|
953
|
+
if (expr.head.methods.length > 0)
|
|
954
|
+
return false;
|
|
955
|
+
return expr.head.primary.type === 'Closure';
|
|
956
|
+
}
|
|
957
|
+
async function evaluateDict(node, ctx) {
|
|
958
|
+
const result = {};
|
|
959
|
+
for (const entry of node.entries) {
|
|
960
|
+
if (isReservedMethod(entry.key)) {
|
|
961
|
+
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: ['keys', 'values', 'entries'] });
|
|
962
|
+
}
|
|
963
|
+
if (isClosureExpr(entry.value)) {
|
|
964
|
+
// Safe cast: isClosureExpr ensures head is PostfixExpr with Closure primary
|
|
965
|
+
const head = entry.value.head;
|
|
966
|
+
const fnLit = head.primary;
|
|
967
|
+
const closure = await createClosure(fnLit, ctx);
|
|
968
|
+
result[entry.key] = closure;
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
result[entry.key] = await evaluateExpression(entry.value, ctx);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
for (const key of Object.keys(result)) {
|
|
975
|
+
const value = result[key];
|
|
976
|
+
if (value !== undefined && isCallable(value)) {
|
|
977
|
+
result[key] = {
|
|
978
|
+
...value,
|
|
979
|
+
boundDict: result,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
async function createClosure(node, ctx) {
|
|
986
|
+
// Store reference to the defining scope for late-bound variable resolution
|
|
987
|
+
const definingScope = ctx;
|
|
988
|
+
const params = [];
|
|
989
|
+
for (const param of node.params) {
|
|
990
|
+
let defaultValue = null;
|
|
991
|
+
if (param.defaultValue) {
|
|
992
|
+
defaultValue = await evaluatePrimary(param.defaultValue, ctx);
|
|
993
|
+
}
|
|
994
|
+
params.push({
|
|
995
|
+
name: param.name,
|
|
996
|
+
typeName: param.typeName,
|
|
997
|
+
defaultValue,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
const isProperty = params.length === 0;
|
|
1001
|
+
return {
|
|
1002
|
+
__type: 'callable',
|
|
1003
|
+
kind: 'script',
|
|
1004
|
+
params,
|
|
1005
|
+
body: node.body,
|
|
1006
|
+
definingScope,
|
|
1007
|
+
isProperty,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
// ============================================================
|
|
1011
|
+
// VARIABLE EVALUATION
|
|
1012
|
+
// ============================================================
|
|
1013
|
+
function getBaseVariableValue(node, ctx) {
|
|
1014
|
+
if (node.isPipeVar) {
|
|
1015
|
+
if (ctx.pipeValue === null) {
|
|
1016
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, 'Undefined variable: $', node.span?.start, { variable: '$' });
|
|
1017
|
+
}
|
|
1018
|
+
return ctx.pipeValue;
|
|
1019
|
+
}
|
|
1020
|
+
if (node.name) {
|
|
1021
|
+
const result = getVariable(ctx, node.name);
|
|
1022
|
+
if (result === undefined) {
|
|
1023
|
+
// Variable doesn't exist - only return null if we'll handle with default/existence check
|
|
1024
|
+
if (node.defaultValue || node.existenceCheck) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, `Undefined variable: $${node.name}`, node.span?.start, { variable: node.name });
|
|
1028
|
+
}
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
function resolveFieldAccess(access, value, ctx) {
|
|
1034
|
+
switch (access.kind) {
|
|
1035
|
+
case 'literal':
|
|
1036
|
+
return access.field;
|
|
1037
|
+
case 'variable': {
|
|
1038
|
+
const varValue = getVariable(ctx, access.variableName);
|
|
1039
|
+
if (typeof varValue === 'string')
|
|
1040
|
+
return varValue;
|
|
1041
|
+
if (typeof varValue === 'number')
|
|
1042
|
+
return varValue;
|
|
1043
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Variable field access requires string or number, got ${typeof varValue}`, undefined, {});
|
|
1044
|
+
}
|
|
1045
|
+
case 'alternatives': {
|
|
1046
|
+
// Try each alternative, return first that exists
|
|
1047
|
+
if (typeof value === 'object' &&
|
|
1048
|
+
value !== null &&
|
|
1049
|
+
!Array.isArray(value)) {
|
|
1050
|
+
const dict = value;
|
|
1051
|
+
for (const alt of access.alternatives) {
|
|
1052
|
+
if (alt in dict)
|
|
1053
|
+
return alt;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return access.alternatives[0] ?? ''; // fallback to first
|
|
1057
|
+
}
|
|
1058
|
+
case 'computed':
|
|
1059
|
+
case 'block':
|
|
1060
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Computed/block field access requires async evaluation`, undefined, {});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function evaluateVariable(node, ctx) {
|
|
1064
|
+
// Note: sync version doesn't support existence checks with computed/block access
|
|
1065
|
+
// Those require async. Simple existence checks are handled here.
|
|
1066
|
+
if (node.existenceCheck) {
|
|
1067
|
+
return evaluateExistenceCheckSync(node, ctx);
|
|
1068
|
+
}
|
|
1069
|
+
let value = getBaseVariableValue(node, ctx);
|
|
1070
|
+
// Use accessChain but skip bracket accesses (require async evaluation)
|
|
1071
|
+
for (const access of node.accessChain) {
|
|
1072
|
+
if (isBracketAccess(access)) {
|
|
1073
|
+
// Bracket accesses require async evaluation - skip in sync context
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (value === null) {
|
|
1077
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot access field on null value`, node.span?.start, {});
|
|
1078
|
+
}
|
|
1079
|
+
const field = resolveFieldAccess(access, value, ctx);
|
|
1080
|
+
value = accessField(value, field);
|
|
1081
|
+
}
|
|
1082
|
+
if (value === null) {
|
|
1083
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Field access returned null`, node.span?.start, {});
|
|
1084
|
+
}
|
|
1085
|
+
return value;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Sync version of existence check for simple (non-computed/block) access.
|
|
1089
|
+
*/
|
|
1090
|
+
function evaluateExistenceCheckSync(node, ctx) {
|
|
1091
|
+
if (!node.existenceCheck)
|
|
1092
|
+
return false;
|
|
1093
|
+
let value = getBaseVariableValue(node, ctx);
|
|
1094
|
+
// Use accessChain but skip bracket accesses (require async evaluation)
|
|
1095
|
+
for (const access of node.accessChain) {
|
|
1096
|
+
if (isBracketAccess(access)) {
|
|
1097
|
+
// Bracket accesses require async evaluation - skip in sync context
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (value === null)
|
|
1101
|
+
return false;
|
|
1102
|
+
const field = resolveFieldAccess(access, value, ctx);
|
|
1103
|
+
value = accessField(value, field);
|
|
1104
|
+
}
|
|
1105
|
+
if (value === null)
|
|
1106
|
+
return false;
|
|
1107
|
+
const finalField = resolveFieldAccess(node.existenceCheck.finalAccess, value, ctx);
|
|
1108
|
+
const finalValue = accessField(value, finalField);
|
|
1109
|
+
if (finalValue === null)
|
|
1110
|
+
return false;
|
|
1111
|
+
if (node.existenceCheck.typeName) {
|
|
1112
|
+
return checkType(finalValue, node.existenceCheck.typeName);
|
|
1113
|
+
}
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
async function evaluateVariableAsync(node, ctx) {
|
|
1117
|
+
// Handle existence check: .?path returns boolean
|
|
1118
|
+
if (node.existenceCheck) {
|
|
1119
|
+
return evaluateExistenceCheck(node, ctx);
|
|
1120
|
+
}
|
|
1121
|
+
let value = getBaseVariableValue(node, ctx);
|
|
1122
|
+
// Apply unified access chain (maintains order of dot and bracket accesses)
|
|
1123
|
+
for (const access of node.accessChain) {
|
|
1124
|
+
// If value is null/missing, either use default or error
|
|
1125
|
+
if (value === null) {
|
|
1126
|
+
if (node.defaultValue) {
|
|
1127
|
+
return evaluateBody(node.defaultValue, ctx);
|
|
1128
|
+
}
|
|
1129
|
+
if (node.existenceCheck) {
|
|
1130
|
+
return false; // .?field returns false for missing
|
|
1131
|
+
}
|
|
1132
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot access field on null value`, node.span?.start, {});
|
|
1133
|
+
}
|
|
1134
|
+
// Check if this is a bracket access
|
|
1135
|
+
if (isBracketAccess(access)) {
|
|
1136
|
+
const indexValue = await evaluatePipeChain(access.expression, ctx);
|
|
1137
|
+
if (typeof indexValue !== 'number') {
|
|
1138
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Bracket index must be number, got ${inferType(indexValue)}`, node.span.start, {});
|
|
1139
|
+
}
|
|
1140
|
+
// Handle negative indices (from end)
|
|
1141
|
+
let index = indexValue;
|
|
1142
|
+
if (index < 0) {
|
|
1143
|
+
if (Array.isArray(value)) {
|
|
1144
|
+
index = value.length + index;
|
|
1145
|
+
}
|
|
1146
|
+
else if (typeof value === 'string') {
|
|
1147
|
+
index = value.length + index;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const prevValue = value;
|
|
1151
|
+
value = accessField(value, index);
|
|
1152
|
+
// If we got null and there's no default, throw specific error
|
|
1153
|
+
if (value === null && !node.defaultValue && !node.existenceCheck) {
|
|
1154
|
+
if (Array.isArray(prevValue)) {
|
|
1155
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `List index out of bounds: ${index}`, node.span?.start, {});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
else {
|
|
1160
|
+
// Field access
|
|
1161
|
+
const field = await resolveFieldAccessAsync(access, value, ctx);
|
|
1162
|
+
const prevValue = value;
|
|
1163
|
+
value = accessField(value, field);
|
|
1164
|
+
if (isCallable(value) && value.isProperty && value.boundDict) {
|
|
1165
|
+
value = await invokeCallable(value, [], ctx, node.span.start);
|
|
1166
|
+
}
|
|
1167
|
+
// If we got null and there's no default, throw specific error
|
|
1168
|
+
if (value === null && !node.defaultValue && !node.existenceCheck) {
|
|
1169
|
+
if (typeof prevValue === 'object' &&
|
|
1170
|
+
prevValue !== null &&
|
|
1171
|
+
!Array.isArray(prevValue) &&
|
|
1172
|
+
!isScriptCallable(prevValue)) {
|
|
1173
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Dict has no field '${field}'`, node.span?.start, {});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// Apply default if final value is null, or error if no default
|
|
1179
|
+
if (value === null) {
|
|
1180
|
+
if (node.defaultValue) {
|
|
1181
|
+
return evaluateBody(node.defaultValue, ctx);
|
|
1182
|
+
}
|
|
1183
|
+
if (node.existenceCheck) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Field access returned null`, node.span?.start, {});
|
|
1187
|
+
}
|
|
1188
|
+
return value;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Type guard to check if a PropertyAccess is a BracketAccess
|
|
1192
|
+
*/
|
|
1193
|
+
function isBracketAccess(access) {
|
|
1194
|
+
return 'accessKind' in access && access.accessKind === 'bracket';
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Evaluate existence check: $data.user.?email or $data.user.?email&string
|
|
1198
|
+
* Returns true if path exists (and optionally matches type), false otherwise.
|
|
1199
|
+
*/
|
|
1200
|
+
async function evaluateExistenceCheck(node, ctx) {
|
|
1201
|
+
if (!node.existenceCheck)
|
|
1202
|
+
return false;
|
|
1203
|
+
let value = getBaseVariableValue(node, ctx);
|
|
1204
|
+
// Traverse the path up to (but not including) the final existence check
|
|
1205
|
+
for (const access of node.accessChain) {
|
|
1206
|
+
if (value === null)
|
|
1207
|
+
return false; // Missing intermediate path
|
|
1208
|
+
if (isBracketAccess(access)) {
|
|
1209
|
+
const indexValue = await evaluatePipeChain(access.expression, ctx);
|
|
1210
|
+
if (typeof indexValue !== 'number')
|
|
1211
|
+
return false;
|
|
1212
|
+
let index = indexValue;
|
|
1213
|
+
if (index < 0) {
|
|
1214
|
+
if (Array.isArray(value)) {
|
|
1215
|
+
index = value.length + index;
|
|
1216
|
+
}
|
|
1217
|
+
else if (typeof value === 'string') {
|
|
1218
|
+
index = value.length + index;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
value = accessField(value, index);
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
const field = await resolveFieldAccessAsync(access, value, ctx);
|
|
1225
|
+
value = accessField(value, field);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
// Now check the final element
|
|
1229
|
+
if (value === null)
|
|
1230
|
+
return false;
|
|
1231
|
+
const finalField = await resolveFieldAccessAsync(node.existenceCheck.finalAccess, value, ctx);
|
|
1232
|
+
const finalValue = accessField(value, finalField);
|
|
1233
|
+
// Check if exists
|
|
1234
|
+
if (finalValue === null)
|
|
1235
|
+
return false;
|
|
1236
|
+
// If type check required, verify type matches
|
|
1237
|
+
if (node.existenceCheck.typeName) {
|
|
1238
|
+
return checkType(finalValue, node.existenceCheck.typeName);
|
|
1239
|
+
}
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
async function resolveFieldAccessAsync(access, value, ctx) {
|
|
1243
|
+
switch (access.kind) {
|
|
1244
|
+
case 'literal':
|
|
1245
|
+
return access.field;
|
|
1246
|
+
case 'variable': {
|
|
1247
|
+
const varValue = getVariable(ctx, access.variableName);
|
|
1248
|
+
if (typeof varValue === 'string')
|
|
1249
|
+
return varValue;
|
|
1250
|
+
if (typeof varValue === 'number')
|
|
1251
|
+
return varValue;
|
|
1252
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Variable field access requires string or number, got ${typeof varValue}`, undefined, {});
|
|
1253
|
+
}
|
|
1254
|
+
case 'alternatives': {
|
|
1255
|
+
if (typeof value === 'object' &&
|
|
1256
|
+
value !== null &&
|
|
1257
|
+
!Array.isArray(value)) {
|
|
1258
|
+
const dict = value;
|
|
1259
|
+
for (const alt of access.alternatives) {
|
|
1260
|
+
if (alt in dict)
|
|
1261
|
+
return alt;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return access.alternatives[0] ?? '';
|
|
1265
|
+
}
|
|
1266
|
+
case 'computed': {
|
|
1267
|
+
const result = await evaluatePipeChain(access.expression, ctx);
|
|
1268
|
+
if (typeof result === 'string')
|
|
1269
|
+
return result;
|
|
1270
|
+
if (typeof result === 'number')
|
|
1271
|
+
return result;
|
|
1272
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Computed field access requires string or number result`, undefined, {});
|
|
1273
|
+
}
|
|
1274
|
+
case 'block': {
|
|
1275
|
+
const result = await evaluateBlock(access.block, ctx);
|
|
1276
|
+
if (typeof result === 'string')
|
|
1277
|
+
return result;
|
|
1278
|
+
if (typeof result === 'number')
|
|
1279
|
+
return result;
|
|
1280
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Block field access requires string or number result`, undefined, {});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function accessField(value, field) {
|
|
1285
|
+
if (value === null)
|
|
1286
|
+
return null;
|
|
1287
|
+
if (typeof field === 'number') {
|
|
1288
|
+
if (Array.isArray(value))
|
|
1289
|
+
return value[field] ?? null;
|
|
1290
|
+
if (typeof value === 'string')
|
|
1291
|
+
return value[field] ?? '';
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
if (typeof value === 'object' &&
|
|
1295
|
+
!Array.isArray(value) &&
|
|
1296
|
+
!isScriptCallable(value)) {
|
|
1297
|
+
return value[field] ?? null;
|
|
1298
|
+
}
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
// ============================================================
|
|
1302
|
+
// FUNCTION & METHOD EVALUATION
|
|
1303
|
+
// ============================================================
|
|
1304
|
+
async function evaluateHostCall(node, ctx) {
|
|
1305
|
+
checkAborted(ctx, node);
|
|
1306
|
+
const fn = ctx.functions.get(node.name);
|
|
1307
|
+
if (!fn) {
|
|
1308
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_FUNCTION, `Unknown function: ${node.name}`, getNodeLocation(node), { functionName: node.name });
|
|
1309
|
+
}
|
|
1310
|
+
const args = await evaluateArgs(node.args, ctx);
|
|
1311
|
+
if (args.length === 0 && ctx.pipeValue !== null) {
|
|
1312
|
+
args.push(ctx.pipeValue);
|
|
1313
|
+
}
|
|
1314
|
+
ctx.observability.onHostCall?.({ name: node.name, args });
|
|
1315
|
+
const startTime = Date.now();
|
|
1316
|
+
const location = getNodeLocation(node);
|
|
1317
|
+
const result = fn(args, ctx, location);
|
|
1318
|
+
let value;
|
|
1319
|
+
if (result instanceof Promise) {
|
|
1320
|
+
value = await withTimeout(result, ctx.timeout, node.name, node);
|
|
1321
|
+
}
|
|
1322
|
+
else {
|
|
1323
|
+
value = result;
|
|
1324
|
+
}
|
|
1325
|
+
ctx.observability.onFunctionReturn?.({
|
|
1326
|
+
name: node.name,
|
|
1327
|
+
value,
|
|
1328
|
+
durationMs: Date.now() - startTime,
|
|
1329
|
+
});
|
|
1330
|
+
return value;
|
|
1331
|
+
}
|
|
1332
|
+
async function evaluateClosureCall(node, ctx) {
|
|
1333
|
+
return evaluateClosureCallWithPipe(node, ctx.pipeValue, ctx);
|
|
1334
|
+
}
|
|
1335
|
+
async function evaluateClosureCallWithPipe(node, pipeInput, ctx) {
|
|
1336
|
+
// Get the base variable
|
|
1337
|
+
let value = getVariable(ctx, node.name);
|
|
1338
|
+
if (value === undefined || value === null) {
|
|
1339
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, `Unknown variable: $${node.name}`, getNodeLocation(node), { variableName: node.name });
|
|
1340
|
+
}
|
|
1341
|
+
// Traverse accessChain to get the closure (e.g., $math.double)
|
|
1342
|
+
const fullPath = ['$' + node.name, ...node.accessChain].join('.');
|
|
1343
|
+
for (const prop of node.accessChain) {
|
|
1344
|
+
if (value === null) {
|
|
1345
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot access property '${prop}' on null in ${fullPath}`, getNodeLocation(node), { property: prop, path: fullPath });
|
|
1346
|
+
}
|
|
1347
|
+
value = accessField(value, prop);
|
|
1348
|
+
}
|
|
1349
|
+
const closure = value;
|
|
1350
|
+
if (!isCallable(closure)) {
|
|
1351
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `${fullPath} is not a function (got ${typeof closure})`, getNodeLocation(node), { path: fullPath, actualType: typeof closure });
|
|
1352
|
+
}
|
|
1353
|
+
const args = await evaluateArgs(node.args, ctx);
|
|
1354
|
+
if (isScriptCallable(closure) &&
|
|
1355
|
+
args.length === 0 &&
|
|
1356
|
+
pipeInput !== null &&
|
|
1357
|
+
closure.params.length > 0) {
|
|
1358
|
+
const firstParam = closure.params[0];
|
|
1359
|
+
if (firstParam?.defaultValue === null && !isCallable(pipeInput)) {
|
|
1360
|
+
args.push(pipeInput);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return invokeCallable(closure, args, ctx, node.span.start);
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Evaluate $.field as property access on the pipe value.
|
|
1367
|
+
* This allows -> $.a to access property 'a' of the current pipe value.
|
|
1368
|
+
*/
|
|
1369
|
+
async function evaluatePipePropertyAccess(node, pipeInput, ctx) {
|
|
1370
|
+
let value = pipeInput;
|
|
1371
|
+
for (const access of node.accessChain) {
|
|
1372
|
+
if (value === null) {
|
|
1373
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot access property on null`, getNodeLocation(node));
|
|
1374
|
+
}
|
|
1375
|
+
if (isBracketAccess(access)) {
|
|
1376
|
+
// Bracket access: [expr]
|
|
1377
|
+
const indexValue = await evaluatePipeChain(access.expression, ctx);
|
|
1378
|
+
if (Array.isArray(value)) {
|
|
1379
|
+
if (typeof indexValue !== 'number') {
|
|
1380
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `List index must be number, got ${inferType(indexValue)}`, getNodeLocation(node));
|
|
1381
|
+
}
|
|
1382
|
+
const result = value[indexValue];
|
|
1383
|
+
if (result === undefined) {
|
|
1384
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `List index out of bounds: ${indexValue}`, getNodeLocation(node));
|
|
1385
|
+
}
|
|
1386
|
+
value = result;
|
|
1387
|
+
}
|
|
1388
|
+
else if (isDict(value)) {
|
|
1389
|
+
if (typeof indexValue !== 'string') {
|
|
1390
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Dict key must be string, got ${inferType(indexValue)}`, getNodeLocation(node));
|
|
1391
|
+
}
|
|
1392
|
+
const result = value[indexValue];
|
|
1393
|
+
if (result === undefined) {
|
|
1394
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Undefined dict key: ${indexValue}`, getNodeLocation(node));
|
|
1395
|
+
}
|
|
1396
|
+
value = result;
|
|
1397
|
+
}
|
|
1398
|
+
else {
|
|
1399
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot index ${inferType(value)}`, getNodeLocation(node));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
// Field access: .field
|
|
1404
|
+
const field = await resolveFieldAccessAsync(access, value, ctx);
|
|
1405
|
+
value = accessField(value, field);
|
|
1406
|
+
if (value === null) {
|
|
1407
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Undefined field: ${field}`, getNodeLocation(node));
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return value;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Evaluate a bare variable as a pipe target: -> $fn
|
|
1415
|
+
* This invokes the closure stored in $fn with the pipe value as argument.
|
|
1416
|
+
* Use :> for captures instead.
|
|
1417
|
+
*/
|
|
1418
|
+
async function evaluateVariableInvoke(node, pipeInput, ctx) {
|
|
1419
|
+
// Handle pipe variable ($) - can't invoke $ as a closure
|
|
1420
|
+
if (node.isPipeVar && !node.name) {
|
|
1421
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot invoke $ as pipe target. Use :> to capture, or $() to invoke $ as closure`, getNodeLocation(node));
|
|
1422
|
+
}
|
|
1423
|
+
// Check if variable exists
|
|
1424
|
+
const rawValue = node.name ? getVariable(ctx, node.name) : null;
|
|
1425
|
+
if (rawValue === undefined) {
|
|
1426
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_VARIABLE, `Unknown variable: $${node.name}`, getNodeLocation(node), { variableName: node.name });
|
|
1427
|
+
}
|
|
1428
|
+
// Get the full variable value (with access chain)
|
|
1429
|
+
const value = evaluateVariable(node, ctx);
|
|
1430
|
+
// Check if it's callable
|
|
1431
|
+
if (!isCallable(value)) {
|
|
1432
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot invoke $${node.name}: expected closure, got ${inferType(value)}. Use :> to capture values.`, getNodeLocation(node));
|
|
1433
|
+
}
|
|
1434
|
+
// Invoke with pipe input as first argument
|
|
1435
|
+
return invokeCallable(value, [pipeInput], ctx, node.span.start);
|
|
1436
|
+
}
|
|
1437
|
+
async function invokeCallable(callable, args, ctx, callLocation) {
|
|
1438
|
+
checkAborted(ctx, undefined);
|
|
1439
|
+
if (callable.kind === 'script') {
|
|
1440
|
+
return invokeScriptCallable(callable, args, ctx, callLocation);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
return invokeFnCallable(callable, args, ctx, callLocation);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async function invokeFnCallable(callable, args, ctx, callLocation) {
|
|
1447
|
+
const effectiveArgs = callable.boundDict && args.length === 0 ? [callable.boundDict] : args;
|
|
1448
|
+
const result = callable.fn(effectiveArgs, ctx, callLocation);
|
|
1449
|
+
return result instanceof Promise ? await result : result;
|
|
1450
|
+
}
|
|
1451
|
+
// ============================================================
|
|
1452
|
+
// CALLABLE INVOCATION HELPERS
|
|
1453
|
+
// ============================================================
|
|
1454
|
+
function createCallableContext(callable, ctx) {
|
|
1455
|
+
// Create a child context with the defining scope as parent
|
|
1456
|
+
// This enables late-bound variable resolution through the scope chain
|
|
1457
|
+
const callableCtx = {
|
|
1458
|
+
...ctx,
|
|
1459
|
+
parent: callable.definingScope,
|
|
1460
|
+
variables: new Map(),
|
|
1461
|
+
variableTypes: new Map(),
|
|
1462
|
+
};
|
|
1463
|
+
if (callable.boundDict) {
|
|
1464
|
+
callableCtx.pipeValue = callable.boundDict;
|
|
1465
|
+
}
|
|
1466
|
+
return callableCtx;
|
|
1467
|
+
}
|
|
1468
|
+
function inferTypeFromDefault(defaultValue) {
|
|
1469
|
+
if (defaultValue === null)
|
|
1470
|
+
return null;
|
|
1471
|
+
const t = inferType(defaultValue);
|
|
1472
|
+
return t === 'string' || t === 'number' || t === 'bool' ? t : null;
|
|
1473
|
+
}
|
|
1474
|
+
function validateParamType(param, value, callLocation) {
|
|
1475
|
+
const expectedType = param.typeName ?? inferTypeFromDefault(param.defaultValue);
|
|
1476
|
+
if (expectedType !== null) {
|
|
1477
|
+
const valueType = inferType(value);
|
|
1478
|
+
if (valueType !== expectedType) {
|
|
1479
|
+
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 });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async function invokeScriptCallable(callable, args, ctx, callLocation) {
|
|
1484
|
+
const firstArg = args[0];
|
|
1485
|
+
if (args.length === 1 && firstArg !== undefined && isTuple(firstArg)) {
|
|
1486
|
+
return invokeScriptCallableWithArgs(callable, firstArg, ctx, callLocation);
|
|
1487
|
+
}
|
|
1488
|
+
const callableCtx = createCallableContext(callable, ctx);
|
|
1489
|
+
for (let i = 0; i < callable.params.length; i++) {
|
|
1490
|
+
const param = callable.params[i];
|
|
1491
|
+
let value;
|
|
1492
|
+
if (i < args.length) {
|
|
1493
|
+
value = args[i];
|
|
1494
|
+
}
|
|
1495
|
+
else if (param.defaultValue !== null) {
|
|
1496
|
+
value = param.defaultValue;
|
|
1497
|
+
}
|
|
1498
|
+
else {
|
|
1499
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Missing argument for parameter '${param.name}' at position ${i}`, callLocation, { paramName: param.name, position: i });
|
|
1500
|
+
}
|
|
1501
|
+
validateParamType(param, value, callLocation);
|
|
1502
|
+
callableCtx.variables.set(param.name, value);
|
|
1503
|
+
}
|
|
1504
|
+
return evaluateBodyExpression(callable.body, callableCtx);
|
|
1505
|
+
}
|
|
1506
|
+
async function invokeScriptCallableWithArgs(closure, tupleValue, ctx, callLocation) {
|
|
1507
|
+
const closureCtx = createCallableContext(closure, ctx);
|
|
1508
|
+
const hasNumericKeys = [...tupleValue.entries.keys()].some((k) => typeof k === 'number');
|
|
1509
|
+
const hasStringKeys = [...tupleValue.entries.keys()].some((k) => typeof k === 'string');
|
|
1510
|
+
if (hasNumericKeys && hasStringKeys) {
|
|
1511
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Tuple cannot mix positional (numeric) and named (string) keys', callLocation);
|
|
1512
|
+
}
|
|
1513
|
+
const boundParams = new Set();
|
|
1514
|
+
if (hasNumericKeys) {
|
|
1515
|
+
for (const [key, value] of tupleValue.entries) {
|
|
1516
|
+
const position = key;
|
|
1517
|
+
const param = closure.params[position];
|
|
1518
|
+
if (param === undefined) {
|
|
1519
|
+
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 });
|
|
1520
|
+
}
|
|
1521
|
+
validateParamType(param, value, callLocation);
|
|
1522
|
+
closureCtx.variables.set(param.name, value);
|
|
1523
|
+
boundParams.add(param.name);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
else if (hasStringKeys) {
|
|
1527
|
+
const paramNames = new Set(closure.params.map((p) => p.name));
|
|
1528
|
+
for (const [key, value] of tupleValue.entries) {
|
|
1529
|
+
const name = key;
|
|
1530
|
+
if (!paramNames.has(name)) {
|
|
1531
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unknown argument '${name}' (valid params: ${[...paramNames].join(', ')})`, callLocation, { argName: name, validParams: [...paramNames] });
|
|
1532
|
+
}
|
|
1533
|
+
const param = closure.params.find((p) => p.name === name);
|
|
1534
|
+
validateParamType(param, value, callLocation);
|
|
1535
|
+
closureCtx.variables.set(name, value);
|
|
1536
|
+
boundParams.add(name);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
for (const param of closure.params) {
|
|
1540
|
+
if (!boundParams.has(param.name)) {
|
|
1541
|
+
if (param.defaultValue !== null) {
|
|
1542
|
+
closureCtx.variables.set(param.name, param.defaultValue);
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Missing argument '${param.name}' (no default value)`, callLocation, { paramName: param.name });
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return evaluateBodyExpression(closure.body, closureCtx);
|
|
1550
|
+
}
|
|
1551
|
+
async function evaluatePipeInvoke(node, input, ctx) {
|
|
1552
|
+
if (!isScriptCallable(input)) {
|
|
1553
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot invoke non-closure value (got ${typeof input})`, getNodeLocation(node));
|
|
1554
|
+
}
|
|
1555
|
+
const args = await evaluateArgs(node.args, ctx);
|
|
1556
|
+
return invokeScriptCallable(input, args, ctx, node.span.start);
|
|
1557
|
+
}
|
|
1558
|
+
async function evaluateMethod(node, receiver, ctx) {
|
|
1559
|
+
checkAborted(ctx, node);
|
|
1560
|
+
// Handle postfix invocation: expr(args) - calls receiver as a closure
|
|
1561
|
+
if (node.type === 'Invoke') {
|
|
1562
|
+
return evaluateInvoke(node, receiver, ctx);
|
|
1563
|
+
}
|
|
1564
|
+
if (isCallable(receiver)) {
|
|
1565
|
+
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' });
|
|
1566
|
+
}
|
|
1567
|
+
const args = await evaluateArgs(node.args, ctx);
|
|
1568
|
+
if (isDict(receiver)) {
|
|
1569
|
+
const dictValue = receiver[node.name];
|
|
1570
|
+
if (dictValue !== undefined && isCallable(dictValue)) {
|
|
1571
|
+
return invokeCallable(dictValue, args, ctx, getNodeLocation(node));
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const method = ctx.methods.get(node.name);
|
|
1575
|
+
if (!method) {
|
|
1576
|
+
// Fall back to property access on dict (no-arg only)
|
|
1577
|
+
if (isDict(receiver) && args.length === 0 && node.name in receiver) {
|
|
1578
|
+
return receiver[node.name];
|
|
1579
|
+
}
|
|
1580
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_UNDEFINED_METHOD, `Unknown method: ${node.name}`, getNodeLocation(node), { methodName: node.name });
|
|
1581
|
+
}
|
|
1582
|
+
const result = method(receiver, args, ctx, getNodeLocation(node));
|
|
1583
|
+
return result instanceof Promise ? await result : result;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Evaluate postfix invocation: expr(args)
|
|
1587
|
+
* Calls the receiver value as a closure with the given arguments.
|
|
1588
|
+
*/
|
|
1589
|
+
async function evaluateInvoke(node, receiver, ctx) {
|
|
1590
|
+
if (!isCallable(receiver)) {
|
|
1591
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot invoke non-callable value (got ${inferType(receiver)})`, getNodeLocation(node), { actualType: inferType(receiver) });
|
|
1592
|
+
}
|
|
1593
|
+
const args = await evaluateArgs(node.args, ctx);
|
|
1594
|
+
return invokeCallable(receiver, args, ctx, getNodeLocation(node));
|
|
1595
|
+
}
|
|
1596
|
+
// ============================================================
|
|
1597
|
+
// CONTROL FLOW EVALUATION
|
|
1598
|
+
// ============================================================
|
|
1599
|
+
async function evaluateConditional(node, ctx) {
|
|
1600
|
+
// Preserve pipe value before evaluating condition (condition may modify it)
|
|
1601
|
+
const savedPipeValue = ctx.pipeValue;
|
|
1602
|
+
let conditionResult;
|
|
1603
|
+
if (node.condition) {
|
|
1604
|
+
const conditionValue = await evaluateBodyExpression(node.condition, ctx);
|
|
1605
|
+
// Condition must be boolean
|
|
1606
|
+
if (typeof conditionValue !== 'boolean') {
|
|
1607
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Conditional expression must be boolean, got ${inferType(conditionValue)}`, node.span.start);
|
|
1608
|
+
}
|
|
1609
|
+
conditionResult = conditionValue;
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
// Piped conditional: $ -> ? then ! else
|
|
1613
|
+
// The pipe value must be boolean
|
|
1614
|
+
if (typeof ctx.pipeValue !== 'boolean') {
|
|
1615
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Piped conditional requires boolean, got ${inferType(ctx.pipeValue)}`, node.span.start);
|
|
1616
|
+
}
|
|
1617
|
+
conditionResult = ctx.pipeValue;
|
|
1618
|
+
}
|
|
1619
|
+
// Restore pipe value for then/else branch evaluation
|
|
1620
|
+
ctx.pipeValue = savedPipeValue;
|
|
1621
|
+
if (conditionResult) {
|
|
1622
|
+
// Create child scope for then branch (reads parent, writes local only)
|
|
1623
|
+
const thenCtx = createChildContext(ctx);
|
|
1624
|
+
thenCtx.pipeValue = savedPipeValue;
|
|
1625
|
+
// Use evaluateBody (not evaluateBodyExpression) so ReturnSignal
|
|
1626
|
+
// propagates up to the containing block rather than being caught here
|
|
1627
|
+
return evaluateBody(node.thenBranch, thenCtx);
|
|
1628
|
+
}
|
|
1629
|
+
else if (node.elseBranch) {
|
|
1630
|
+
// Create child scope for else branch (reads parent, writes local only)
|
|
1631
|
+
const elseCtx = createChildContext(ctx);
|
|
1632
|
+
elseCtx.pipeValue = savedPipeValue;
|
|
1633
|
+
if (node.elseBranch.type === 'Conditional') {
|
|
1634
|
+
return evaluateConditional(node.elseBranch, elseCtx);
|
|
1635
|
+
}
|
|
1636
|
+
return evaluateBody(node.elseBranch, elseCtx);
|
|
1637
|
+
}
|
|
1638
|
+
return ctx.pipeValue;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* While loop evaluation: cond @ body
|
|
1642
|
+
* Condition must evaluate to boolean. Re-evaluated each iteration.
|
|
1643
|
+
* Each iteration creates a child scope (reads parent, writes local only).
|
|
1644
|
+
* For iteration over collections, use `each` operator instead.
|
|
1645
|
+
*/
|
|
1646
|
+
async function evaluateWhileLoop(node, ctx) {
|
|
1647
|
+
// Save original pipe value before evaluating condition
|
|
1648
|
+
const originalPipeValue = ctx.pipeValue;
|
|
1649
|
+
// Evaluate condition
|
|
1650
|
+
const conditionValue = await evaluateExpression(node.condition, ctx);
|
|
1651
|
+
// Restore original pipe value for loop body
|
|
1652
|
+
ctx.pipeValue = originalPipeValue;
|
|
1653
|
+
// Condition must be boolean
|
|
1654
|
+
if (typeof conditionValue !== 'boolean') {
|
|
1655
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `While loop condition must be boolean, got ${typeof conditionValue}`, getNodeLocation(node));
|
|
1656
|
+
}
|
|
1657
|
+
let value = ctx.pipeValue;
|
|
1658
|
+
let iterCount = 0;
|
|
1659
|
+
const maxIter = getIterationLimit(ctx);
|
|
1660
|
+
try {
|
|
1661
|
+
let conditionResult = conditionValue;
|
|
1662
|
+
while (conditionResult) {
|
|
1663
|
+
iterCount++;
|
|
1664
|
+
if (iterCount > maxIter) {
|
|
1665
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_LIMIT_EXCEEDED, `While loop exceeded ${maxIter} iterations`, getNodeLocation(node), { limit: maxIter, iterations: iterCount });
|
|
1666
|
+
}
|
|
1667
|
+
checkAborted(ctx, node);
|
|
1668
|
+
// Create child scope for this iteration
|
|
1669
|
+
const iterCtx = createChildContext(ctx);
|
|
1670
|
+
iterCtx.pipeValue = value;
|
|
1671
|
+
value = await evaluateBody(node.body, iterCtx);
|
|
1672
|
+
ctx.pipeValue = value;
|
|
1673
|
+
// Re-evaluate condition for next iteration
|
|
1674
|
+
const nextCondition = await evaluateExpression(node.condition, ctx);
|
|
1675
|
+
if (typeof nextCondition !== 'boolean') {
|
|
1676
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `While loop condition must be boolean, got ${typeof nextCondition}`, getNodeLocation(node));
|
|
1677
|
+
}
|
|
1678
|
+
conditionResult = nextCondition;
|
|
1679
|
+
// Restore pipeValue after condition evaluation
|
|
1680
|
+
ctx.pipeValue = value;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
catch (e) {
|
|
1684
|
+
if (e instanceof BreakSignal) {
|
|
1685
|
+
return e.value;
|
|
1686
|
+
}
|
|
1687
|
+
throw e;
|
|
1688
|
+
}
|
|
1689
|
+
return value;
|
|
1690
|
+
}
|
|
1691
|
+
async function evaluateDoWhileLoop(node, ctx) {
|
|
1692
|
+
let value = ctx.pipeValue;
|
|
1693
|
+
try {
|
|
1694
|
+
// Do-while: body executes first, then condition is checked
|
|
1695
|
+
// Each iteration creates a child scope (reads parent, writes local only)
|
|
1696
|
+
let shouldContinue = true;
|
|
1697
|
+
while (shouldContinue) {
|
|
1698
|
+
checkAborted(ctx, node);
|
|
1699
|
+
const iterCtx = createChildContext(ctx);
|
|
1700
|
+
iterCtx.pipeValue = value;
|
|
1701
|
+
value = await evaluateBody(node.body, iterCtx);
|
|
1702
|
+
ctx.pipeValue = value;
|
|
1703
|
+
const conditionValue = await evaluateBodyExpression(node.condition, ctx);
|
|
1704
|
+
// Condition must be boolean
|
|
1705
|
+
if (typeof conditionValue !== 'boolean') {
|
|
1706
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Do-while condition must be boolean, got ${inferType(conditionValue)}`, getNodeLocation(node));
|
|
1707
|
+
}
|
|
1708
|
+
shouldContinue = conditionValue;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
catch (e) {
|
|
1712
|
+
if (e instanceof BreakSignal) {
|
|
1713
|
+
return e.value;
|
|
1714
|
+
}
|
|
1715
|
+
throw e;
|
|
1716
|
+
}
|
|
1717
|
+
return value;
|
|
1718
|
+
}
|
|
1719
|
+
async function evaluateBlock(node, ctx) {
|
|
1720
|
+
// Create child scope for the block: reads from parent, writes to local only
|
|
1721
|
+
const blockCtx = createChildContext(ctx);
|
|
1722
|
+
// All siblings inherit the SAME $ from parent (captured when block entered)
|
|
1723
|
+
const parentPipeValue = blockCtx.pipeValue;
|
|
1724
|
+
let lastValue = parentPipeValue;
|
|
1725
|
+
for (const stmt of node.statements) {
|
|
1726
|
+
// Each statement gets fresh child context with parent's $
|
|
1727
|
+
// This ensures siblings don't share $ - each sees the block's $
|
|
1728
|
+
const stmtCtx = createChildContext(blockCtx);
|
|
1729
|
+
stmtCtx.pipeValue = parentPipeValue; // Always parent's $, not previous sibling's
|
|
1730
|
+
lastValue = await executeStatement(stmt, stmtCtx);
|
|
1731
|
+
// Variables captured via -> need to be promoted to block scope
|
|
1732
|
+
// so they're visible to later siblings
|
|
1733
|
+
for (const [name, value] of stmtCtx.variables) {
|
|
1734
|
+
if (!blockCtx.variables.has(name)) {
|
|
1735
|
+
blockCtx.variables.set(name, value);
|
|
1736
|
+
const varType = stmtCtx.variableTypes.get(name);
|
|
1737
|
+
if (varType !== undefined) {
|
|
1738
|
+
blockCtx.variableTypes.set(name, varType);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return lastValue; // Last sibling's result is block result
|
|
1744
|
+
}
|
|
1745
|
+
async function evaluateBlockExpression(node, ctx) {
|
|
1746
|
+
try {
|
|
1747
|
+
return await evaluateBlock(node, ctx);
|
|
1748
|
+
}
|
|
1749
|
+
catch (e) {
|
|
1750
|
+
if (e instanceof ReturnSignal) {
|
|
1751
|
+
return e.value;
|
|
1752
|
+
}
|
|
1753
|
+
throw e;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Evaluate a simple body (Block, GroupedExpr, or PostfixExpr).
|
|
1758
|
+
* Used by conditionals and loops.
|
|
1759
|
+
*/
|
|
1760
|
+
async function evaluateBody(node, ctx) {
|
|
1761
|
+
switch (node.type) {
|
|
1762
|
+
case 'Block':
|
|
1763
|
+
return evaluateBlock(node, ctx);
|
|
1764
|
+
case 'GroupedExpr':
|
|
1765
|
+
return evaluateGroupedExpr(node, ctx);
|
|
1766
|
+
case 'PostfixExpr':
|
|
1767
|
+
return evaluatePostfixExpr(node, ctx);
|
|
1768
|
+
case 'PipeChain':
|
|
1769
|
+
return evaluatePipeChain(node, ctx);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Evaluate a simple body as an expression (catches ReturnSignal).
|
|
1774
|
+
*/
|
|
1775
|
+
async function evaluateBodyExpression(node, ctx) {
|
|
1776
|
+
try {
|
|
1777
|
+
return await evaluateBody(node, ctx);
|
|
1778
|
+
}
|
|
1779
|
+
catch (e) {
|
|
1780
|
+
if (e instanceof ReturnSignal) {
|
|
1781
|
+
return e.value;
|
|
1782
|
+
}
|
|
1783
|
+
throw e;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
// ============================================================
|
|
1787
|
+
// EXPRESSION EVALUATION (arithmetic, comparison, logical)
|
|
1788
|
+
// ============================================================
|
|
1789
|
+
async function evaluateBinaryExpr(node, ctx) {
|
|
1790
|
+
const { op } = node;
|
|
1791
|
+
// Logical operators with short-circuit evaluation
|
|
1792
|
+
if (op === '||') {
|
|
1793
|
+
const left = await evaluateExprHead(node.left, ctx);
|
|
1794
|
+
if (isTruthy(left))
|
|
1795
|
+
return true;
|
|
1796
|
+
const right = await evaluateExprHead(node.right, ctx);
|
|
1797
|
+
return isTruthy(right);
|
|
1798
|
+
}
|
|
1799
|
+
if (op === '&&') {
|
|
1800
|
+
const left = await evaluateExprHead(node.left, ctx);
|
|
1801
|
+
if (!isTruthy(left))
|
|
1802
|
+
return false;
|
|
1803
|
+
const right = await evaluateExprHead(node.right, ctx);
|
|
1804
|
+
return isTruthy(right);
|
|
1805
|
+
}
|
|
1806
|
+
// Comparison operators - work on any values, return boolean
|
|
1807
|
+
if (op === '==' ||
|
|
1808
|
+
op === '!=' ||
|
|
1809
|
+
op === '<' ||
|
|
1810
|
+
op === '>' ||
|
|
1811
|
+
op === '<=' ||
|
|
1812
|
+
op === '>=') {
|
|
1813
|
+
const left = await evaluateExprHead(node.left, ctx);
|
|
1814
|
+
const right = await evaluateExprHead(node.right, ctx);
|
|
1815
|
+
return evaluateBinaryComparison(left, right, op, node);
|
|
1816
|
+
}
|
|
1817
|
+
// Arithmetic operators - require numbers
|
|
1818
|
+
const left = await evaluateExprHeadNumber(node.left, ctx);
|
|
1819
|
+
const right = await evaluateExprHeadNumber(node.right, ctx);
|
|
1820
|
+
switch (op) {
|
|
1821
|
+
case '+':
|
|
1822
|
+
return left + right;
|
|
1823
|
+
case '-':
|
|
1824
|
+
return left - right;
|
|
1825
|
+
case '*':
|
|
1826
|
+
return left * right;
|
|
1827
|
+
case '/':
|
|
1828
|
+
if (right === 0) {
|
|
1829
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Division by zero', node.span.start);
|
|
1830
|
+
}
|
|
1831
|
+
return left / right;
|
|
1832
|
+
case '%':
|
|
1833
|
+
if (right === 0) {
|
|
1834
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, 'Modulo by zero', node.span.start);
|
|
1835
|
+
}
|
|
1836
|
+
return left % right;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/** Evaluate comparison between two values */
|
|
1840
|
+
function evaluateBinaryComparison(left, right, op, node) {
|
|
1841
|
+
switch (op) {
|
|
1842
|
+
case '==':
|
|
1843
|
+
return deepEquals(left, right);
|
|
1844
|
+
case '!=':
|
|
1845
|
+
return !deepEquals(left, right);
|
|
1846
|
+
case '<':
|
|
1847
|
+
case '>':
|
|
1848
|
+
case '<=':
|
|
1849
|
+
case '>=':
|
|
1850
|
+
// Ordering comparisons require compatible types
|
|
1851
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
1852
|
+
return op === '<'
|
|
1853
|
+
? left < right
|
|
1854
|
+
: op === '>'
|
|
1855
|
+
? left > right
|
|
1856
|
+
: op === '<='
|
|
1857
|
+
? left <= right
|
|
1858
|
+
: left >= right;
|
|
1859
|
+
}
|
|
1860
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
1861
|
+
return op === '<'
|
|
1862
|
+
? left < right
|
|
1863
|
+
: op === '>'
|
|
1864
|
+
? left > right
|
|
1865
|
+
: op === '<='
|
|
1866
|
+
? left <= right
|
|
1867
|
+
: left >= right;
|
|
1868
|
+
}
|
|
1869
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Cannot compare ${inferType(left)} with ${inferType(right)} using ${op}`, node.span.start);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async function evaluateUnaryExpr(node, ctx) {
|
|
1873
|
+
if (node.op === '!') {
|
|
1874
|
+
const value = await evaluateExprHead(node.operand, ctx);
|
|
1875
|
+
return !isTruthy(value);
|
|
1876
|
+
}
|
|
1877
|
+
// Unary minus
|
|
1878
|
+
const operand = node.operand;
|
|
1879
|
+
if (operand.type === 'UnaryExpr') {
|
|
1880
|
+
const inner = await evaluateUnaryExpr(operand, ctx);
|
|
1881
|
+
if (typeof inner !== 'number') {
|
|
1882
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Unary minus requires number, got ${inferType(inner)}`, node.span.start);
|
|
1883
|
+
}
|
|
1884
|
+
return -inner;
|
|
1885
|
+
}
|
|
1886
|
+
const value = await evaluateExprHeadNumber(operand, ctx);
|
|
1887
|
+
return -value;
|
|
1888
|
+
}
|
|
1889
|
+
/** Evaluate expression head, returning any RillValue */
|
|
1890
|
+
async function evaluateExprHead(node, ctx) {
|
|
1891
|
+
switch (node.type) {
|
|
1892
|
+
case 'BinaryExpr':
|
|
1893
|
+
return evaluateBinaryExpr(node, ctx);
|
|
1894
|
+
case 'UnaryExpr':
|
|
1895
|
+
return evaluateUnaryExpr(node, ctx);
|
|
1896
|
+
case 'PostfixExpr':
|
|
1897
|
+
return evaluatePostfixExpr(node, ctx);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/** Evaluate expression head, requiring a number result */
|
|
1901
|
+
async function evaluateExprHeadNumber(node, ctx) {
|
|
1902
|
+
const value = await evaluateExprHead(node, ctx);
|
|
1903
|
+
if (typeof value !== 'number') {
|
|
1904
|
+
throw new RuntimeError(RILL_ERROR_CODES.RUNTIME_TYPE_ERROR, `Arithmetic requires number, got ${inferType(value)}`, node.span.start);
|
|
1905
|
+
}
|
|
1906
|
+
return value;
|
|
1907
|
+
}
|
|
1908
|
+
async function evaluateGroupedExpr(node, ctx) {
|
|
1909
|
+
// Grouped expressions have their own scope (reads parent, writes local only)
|
|
1910
|
+
const childCtx = createChildContext(ctx);
|
|
1911
|
+
return evaluatePipeChain(node.expression, childCtx);
|
|
1912
|
+
}
|
|
1913
|
+
//# sourceMappingURL=evaluate.js.map
|