@rcrsr/rill 0.1.0 → 0.2.0

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