@telorun/cel 0.0.0-stage → 0.107.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 (223) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +281 -2
  3. package/dist/activation.d.ts +27 -0
  4. package/dist/activation.d.ts.map +1 -0
  5. package/dist/activation.js +24 -0
  6. package/dist/backend-runtime.d.ts +146 -0
  7. package/dist/backend-runtime.d.ts.map +1 -0
  8. package/dist/backend-runtime.js +322 -0
  9. package/dist/bounded-cache.d.ts +21 -0
  10. package/dist/bounded-cache.d.ts.map +1 -0
  11. package/dist/bounded-cache.js +42 -0
  12. package/dist/catalog-runtime.d.ts +59 -0
  13. package/dist/catalog-runtime.d.ts.map +1 -0
  14. package/dist/catalog-runtime.js +785 -0
  15. package/dist/cel-expression.d.ts +32 -0
  16. package/dist/cel-expression.d.ts.map +1 -0
  17. package/dist/cel-expression.js +29 -0
  18. package/dist/cel-map-value.d.ts +34 -0
  19. package/dist/cel-map-value.d.ts.map +1 -0
  20. package/dist/cel-map-value.js +74 -0
  21. package/dist/cel-program.d.ts +44 -0
  22. package/dist/cel-program.d.ts.map +1 -0
  23. package/dist/cel-program.js +72 -0
  24. package/dist/cel-type.d.ts +131 -0
  25. package/dist/cel-type.d.ts.map +1 -0
  26. package/dist/cel-type.js +293 -0
  27. package/dist/cel-value.d.ts +159 -0
  28. package/dist/cel-value.d.ts.map +1 -0
  29. package/dist/cel-value.js +225 -0
  30. package/dist/check-diagnostic.d.ts +53 -0
  31. package/dist/check-diagnostic.d.ts.map +1 -0
  32. package/dist/check-diagnostic.js +66 -0
  33. package/dist/checker.d.ts +77 -0
  34. package/dist/checker.d.ts.map +1 -0
  35. package/dist/checker.js +721 -0
  36. package/dist/closure-backend.d.ts +21 -0
  37. package/dist/closure-backend.d.ts.map +1 -0
  38. package/dist/closure-backend.js +436 -0
  39. package/dist/comprehension-bindings.d.ts +33 -0
  40. package/dist/comprehension-bindings.d.ts.map +1 -0
  41. package/dist/comprehension-bindings.js +50 -0
  42. package/dist/comprehension-runtime.d.ts +44 -0
  43. package/dist/comprehension-runtime.d.ts.map +1 -0
  44. package/dist/comprehension-runtime.js +137 -0
  45. package/dist/declared-chain.d.ts +35 -0
  46. package/dist/declared-chain.d.ts.map +1 -0
  47. package/dist/declared-chain.js +36 -0
  48. package/dist/duration-value.d.ts +59 -0
  49. package/dist/duration-value.d.ts.map +1 -0
  50. package/dist/duration-value.js +135 -0
  51. package/dist/emitted-module.d.ts +207 -0
  52. package/dist/emitted-module.d.ts.map +1 -0
  53. package/dist/emitted-module.js +359 -0
  54. package/dist/engine-version.d.ts +3 -0
  55. package/dist/engine-version.d.ts.map +1 -0
  56. package/dist/engine-version.js +8 -0
  57. package/dist/environment-digest.d.ts +44 -0
  58. package/dist/environment-digest.d.ts.map +1 -0
  59. package/dist/environment-digest.js +98 -0
  60. package/dist/environment.d.ts +283 -0
  61. package/dist/environment.d.ts.map +1 -0
  62. package/dist/environment.js +459 -0
  63. package/dist/function-catalog.d.ts +66 -0
  64. package/dist/function-catalog.d.ts.map +1 -0
  65. package/dist/function-catalog.js +77 -0
  66. package/dist/function-registry.d.ts +78 -0
  67. package/dist/function-registry.d.ts.map +1 -0
  68. package/dist/function-registry.js +189 -0
  69. package/dist/index.d.ts +91 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +59 -0
  72. package/dist/integer-arithmetic.d.ts +27 -0
  73. package/dist/integer-arithmetic.d.ts.map +1 -0
  74. package/dist/integer-arithmetic.js +58 -0
  75. package/dist/js-emitter.d.ts +132 -0
  76. package/dist/js-emitter.d.ts.map +1 -0
  77. package/dist/js-emitter.js +562 -0
  78. package/dist/json-schema-type.d.ts +182 -0
  79. package/dist/json-schema-type.d.ts.map +1 -0
  80. package/dist/json-schema-type.js +487 -0
  81. package/dist/json-text-scan.d.ts +28 -0
  82. package/dist/json-text-scan.d.ts.map +1 -0
  83. package/dist/json-text-scan.js +159 -0
  84. package/dist/lexer.d.ts +103 -0
  85. package/dist/lexer.d.ts.map +1 -0
  86. package/dist/lexer.js +458 -0
  87. package/dist/macro-check.d.ts +33 -0
  88. package/dist/macro-check.d.ts.map +1 -0
  89. package/dist/macro-check.js +162 -0
  90. package/dist/macro-shape.d.ts +24 -0
  91. package/dist/macro-shape.d.ts.map +1 -0
  92. package/dist/macro-shape.js +55 -0
  93. package/dist/member-read.d.ts +52 -0
  94. package/dist/member-read.d.ts.map +1 -0
  95. package/dist/member-read.js +125 -0
  96. package/dist/namespace-resolution.d.ts +35 -0
  97. package/dist/namespace-resolution.d.ts.map +1 -0
  98. package/dist/namespace-resolution.js +160 -0
  99. package/dist/nominal-type.d.ts +63 -0
  100. package/dist/nominal-type.d.ts.map +1 -0
  101. package/dist/nominal-type.js +98 -0
  102. package/dist/nullable-access.d.ts +38 -0
  103. package/dist/nullable-access.d.ts.map +1 -0
  104. package/dist/nullable-access.js +93 -0
  105. package/dist/parse-limits.d.ts +26 -0
  106. package/dist/parse-limits.d.ts.map +1 -0
  107. package/dist/parse-limits.js +21 -0
  108. package/dist/parser.d.ts +48 -0
  109. package/dist/parser.d.ts.map +1 -0
  110. package/dist/parser.js +503 -0
  111. package/dist/qualified-calls.d.ts +22 -0
  112. package/dist/qualified-calls.d.ts.map +1 -0
  113. package/dist/qualified-calls.js +27 -0
  114. package/dist/regular-expression.d.ts +48 -0
  115. package/dist/regular-expression.d.ts.map +1 -0
  116. package/dist/regular-expression.js +77 -0
  117. package/dist/reserved-words.d.ts +52 -0
  118. package/dist/reserved-words.d.ts.map +1 -0
  119. package/dist/reserved-words.js +77 -0
  120. package/dist/resolved-call.d.ts +33 -0
  121. package/dist/resolved-call.d.ts.map +1 -0
  122. package/dist/resolved-call.js +15 -0
  123. package/dist/root-references.d.ts +23 -0
  124. package/dist/root-references.d.ts.map +1 -0
  125. package/dist/root-references.js +120 -0
  126. package/dist/runtime-library.d.ts +56 -0
  127. package/dist/runtime-library.d.ts.map +1 -0
  128. package/dist/runtime-library.js +545 -0
  129. package/dist/serializer.d.ts +24 -0
  130. package/dist/serializer.d.ts.map +1 -0
  131. package/dist/serializer.js +240 -0
  132. package/dist/sha256.d.ts +20 -0
  133. package/dist/sha256.d.ts.map +1 -0
  134. package/dist/sha256.js +103 -0
  135. package/dist/signature.d.ts +72 -0
  136. package/dist/signature.d.ts.map +1 -0
  137. package/dist/signature.js +61 -0
  138. package/dist/signatures/function-catalog.json +788 -0
  139. package/dist/signatures/standard-library.json +229 -0
  140. package/dist/standard-library.d.ts +41 -0
  141. package/dist/standard-library.d.ts.map +1 -0
  142. package/dist/standard-library.js +85 -0
  143. package/dist/syntax-diagnostic.d.ts +61 -0
  144. package/dist/syntax-diagnostic.d.ts.map +1 -0
  145. package/dist/syntax-diagnostic.js +28 -0
  146. package/dist/syntax-tree.d.ts +160 -0
  147. package/dist/syntax-tree.d.ts.map +1 -0
  148. package/dist/syntax-tree.js +58 -0
  149. package/dist/timestamp-value.d.ts +53 -0
  150. package/dist/timestamp-value.d.ts.map +1 -0
  151. package/dist/timestamp-value.js +223 -0
  152. package/dist/tree-equality.d.ts +15 -0
  153. package/dist/tree-equality.d.ts.map +1 -0
  154. package/dist/tree-equality.js +105 -0
  155. package/dist/type-expression.d.ts +26 -0
  156. package/dist/type-expression.d.ts.map +1 -0
  157. package/dist/type-expression.js +134 -0
  158. package/dist/value-equality.d.ts +38 -0
  159. package/dist/value-equality.d.ts.map +1 -0
  160. package/dist/value-equality.js +196 -0
  161. package/dist/value-text.d.ts +25 -0
  162. package/dist/value-text.d.ts.map +1 -0
  163. package/dist/value-text.js +44 -0
  164. package/dist/zoned-calendar.d.ts +61 -0
  165. package/dist/zoned-calendar.d.ts.map +1 -0
  166. package/dist/zoned-calendar.js +143 -0
  167. package/package.json +56 -3
  168. package/src/activation.ts +32 -0
  169. package/src/backend-runtime.ts +454 -0
  170. package/src/bounded-cache.ts +45 -0
  171. package/src/catalog-runtime.ts +921 -0
  172. package/src/cel-expression.ts +53 -0
  173. package/src/cel-map-value.ts +86 -0
  174. package/src/cel-program.ts +103 -0
  175. package/src/cel-type.ts +359 -0
  176. package/src/cel-value.ts +353 -0
  177. package/src/check-diagnostic.ts +102 -0
  178. package/src/checker.ts +932 -0
  179. package/src/closure-backend.ts +502 -0
  180. package/src/comprehension-bindings.ts +66 -0
  181. package/src/comprehension-runtime.ts +157 -0
  182. package/src/declared-chain.ts +45 -0
  183. package/src/duration-value.ts +145 -0
  184. package/src/emitted-module.ts +494 -0
  185. package/src/engine-version.ts +9 -0
  186. package/src/environment-digest.ts +111 -0
  187. package/src/environment.ts +740 -0
  188. package/src/function-catalog.ts +140 -0
  189. package/src/function-registry.ts +229 -0
  190. package/src/index.ts +386 -0
  191. package/src/integer-arithmetic.ts +64 -0
  192. package/src/js-emitter.ts +713 -0
  193. package/src/json-schema-type.ts +664 -0
  194. package/src/json-text-scan.ts +163 -0
  195. package/src/lexer.ts +562 -0
  196. package/src/macro-check.ts +191 -0
  197. package/src/macro-shape.ts +66 -0
  198. package/src/member-read.ts +126 -0
  199. package/src/namespace-resolution.ts +167 -0
  200. package/src/nominal-type.ts +149 -0
  201. package/src/nullable-access.ts +95 -0
  202. package/src/parse-limits.ts +36 -0
  203. package/src/parser.ts +554 -0
  204. package/src/qualified-calls.ts +39 -0
  205. package/src/regular-expression.ts +101 -0
  206. package/src/reserved-words.ts +94 -0
  207. package/src/resolved-call.ts +34 -0
  208. package/src/root-references.ts +126 -0
  209. package/src/runtime-library.ts +615 -0
  210. package/src/serializer.ts +246 -0
  211. package/src/sha256.ts +112 -0
  212. package/src/signature.ts +127 -0
  213. package/src/signatures/function-catalog.json +788 -0
  214. package/src/signatures/standard-library.json +235 -0
  215. package/src/standard-library.ts +149 -0
  216. package/src/syntax-diagnostic.ts +72 -0
  217. package/src/syntax-tree.ts +229 -0
  218. package/src/timestamp-value.ts +294 -0
  219. package/src/tree-equality.ts +130 -0
  220. package/src/type-expression.ts +160 -0
  221. package/src/value-equality.ts +201 -0
  222. package/src/value-text.ts +45 -0
  223. package/src/zoned-calendar.ts +178 -0
@@ -0,0 +1,713 @@
1
+ /**
2
+ * The JavaScript emitter: a tree compiled to **source**, not to closures.
3
+ *
4
+ * It decides nothing the closure backend decides differently. Every operator and standard
5
+ * function is the runtime library's, every comprehension is `comprehension-runtime.ts`, and
6
+ * every member read, name read, bool operand and call-site dispatch is `backend-runtime.ts`
7
+ * — so what this file produces is a tree of CALLS into those, in the same order, with the
8
+ * same short-circuit. That is why the two backends can be held to one answer case for case:
9
+ * the only thing that differs is how control gets from one call to the next.
10
+ *
11
+ * Three properties the shape of the output exists for:
12
+ *
13
+ * - **The runtime is a parameter, never an import.** The emitted module's default export is
14
+ * a factory; it names no specifier, so it loads from a `data:` URL, from a cache directory
15
+ * mounted anywhere, and under a host that resolves nothing. A module naming
16
+ * `@telorun/cel` would also silently accept a runtime of another version, which the cache
17
+ * key could not see.
18
+ * - **No bare property access implements a CEL read.** `a.b`, `a['b']`, `a[expr]`, `.?`,
19
+ * `[?]` and `has()` all emit a call to the member-read seam, so no key an author or a
20
+ * request wrote reaches a prototype, a method or a function property. The frame's own
21
+ * fields are the engine's structure and are read directly, as they are in a closure.
22
+ * - **Nothing is asynchronous.** No `async`, no `await`, no promise: a thenable is refused
23
+ * at the same doors, because the doors are the shared runtime's.
24
+ *
25
+ * Emission is **deterministic**: temporaries are numbered per emitted function in tree
26
+ * order, bindings per module in tree order, and every hoisted constant is keyed by its own
27
+ * text, so the same tree against the same environment is the same bytes.
28
+ */
29
+
30
+ import {
31
+ CelCompileError,
32
+ plainMemberChain,
33
+ prefixCandidates,
34
+ type CompileTarget,
35
+ } from "./backend-runtime.js";
36
+ import { namespaceMacroBinding, receiverMacroBinding } from "./comprehension-bindings.js";
37
+ import { splitDeclaredChain } from "./declared-chain.js";
38
+ import { isMacroCall } from "./macro-check.js";
39
+ import type { CelLiteral, CelNode, CelSelectNode, SourceRange } from "./syntax-tree.js";
40
+
41
+ /**
42
+ * The names an emitted module destructures from the runtime it is handed, in the order it
43
+ * destructures them. It is the module's whole contract with the engine: `emitterRuntime`
44
+ * (`emitted-module.ts`) builds an object with exactly these keys, and a module emitted by
45
+ * one engine against another's runtime fails at load rather than running on a missing
46
+ * binding — which is the second reason the key carries the engine version.
47
+ */
48
+ export const RUNTIME_BINDINGS = [
49
+ "asyncValueRefused",
50
+ "boolOperand",
51
+ "callSite",
52
+ "celAll",
53
+ "celError",
54
+ "celExists",
55
+ "celExistsOne",
56
+ "celFilter",
57
+ "celIterable",
58
+ "celMapComprehension",
59
+ "celMapFromEntries",
60
+ "celSome",
61
+ "celUint",
62
+ "constants",
63
+ "hasMember",
64
+ "isCelError",
65
+ "isCelOptional",
66
+ "none",
67
+ "optionalEntry",
68
+ "optionalOfNonZero",
69
+ "readHostValue",
70
+ "readName",
71
+ "readNameChain",
72
+ "readThrough",
73
+ "searchNameChain",
74
+ ] as const;
75
+
76
+ export type RuntimeBinding = (typeof RUNTIME_BINDINGS)[number];
77
+
78
+ /** A name a macro bound, and the JavaScript name its value lives under. */
79
+ interface Scope {
80
+ readonly name: string;
81
+ readonly js: string;
82
+ readonly outer: Scope | undefined;
83
+ }
84
+
85
+ function boundName(scope: Scope | undefined, name: string): string | undefined {
86
+ for (let at = scope; at; at = at.outer) if (at.name === name) return at.js;
87
+ return undefined;
88
+ }
89
+
90
+ /**
91
+ * The temporaries one emitted function declares. Each emitted function — the expression's
92
+ * own, and every comprehension or binding body inside it — declares its own, because a
93
+ * body's closure outlives the statement that called it and a name it shares with its caller
94
+ * would clobber a value still in use.
95
+ *
96
+ * **Every name is unique across the MODULE, not within the function**, and that is not
97
+ * tidiness: a body declaring `let t0` would **shadow** the `t0` its caller holds a bound
98
+ * value in, so `cel.bind(n, 2, xs.map(e, e + n))` read the element where it meant the
99
+ * binding and answered `[2, 4, 6]` for `[3, 4, 5]`. Numbering per module makes the shape
100
+ * impossible rather than avoided.
101
+ */
102
+ class Temporaries {
103
+ private readonly names: string[] = [];
104
+
105
+ constructor(private readonly allocate: () => string) {}
106
+
107
+ next(): string {
108
+ const name = this.allocate();
109
+ this.names.push(name);
110
+ return name;
111
+ }
112
+
113
+ /** The `let` that declares them, or nothing where the function used none. */
114
+ declaration(): string {
115
+ return this.names.length === 0 ? "" : `let ${this.names.join(", ")}; `;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * One module's worth of emission. The hoist table is shared by every expression in the
121
+ * module, so two expressions reading the same name share one range and one rest-list: a
122
+ * constant allocated per evaluation is the cost the closure backend does not pay, and
123
+ * hoisting is how the emitter does not pay it either.
124
+ */
125
+ export class ModuleEmitter {
126
+ private readonly hoisted: string[] = [];
127
+ private readonly hoistedByText = new Map<string, string>();
128
+ private locals = 0;
129
+ private bindings = 0;
130
+
131
+ constructor(private readonly target: CompileTarget) {}
132
+
133
+ /** The source of one expression's function: a `CelStep` over the evaluation frame. */
134
+ emitFunction(root: CelNode): string {
135
+ const temporaries = this.temporaries();
136
+ const body = this.node(root, undefined, temporaries);
137
+ return `(frame) => { ${temporaries.declaration()}return ${body}; }`;
138
+ }
139
+
140
+ /** A fresh function's temporaries, drawing names from the module's own numbering. */
141
+ private temporaries(): Temporaries {
142
+ return new Temporaries(() => {
143
+ const name = `t${this.locals}`;
144
+ this.locals += 1;
145
+ return name;
146
+ });
147
+ }
148
+
149
+ /** The `const` lines every emitted expression reads, in first-use order. */
150
+ hoistedLines(): readonly string[] {
151
+ return this.hoisted;
152
+ }
153
+
154
+ private hoist(text: string): string {
155
+ const held = this.hoistedByText.get(text);
156
+ if (held !== undefined) return held;
157
+ const name = `h${this.hoisted.length}`;
158
+ this.hoisted.push(`const ${name} = ${text};`);
159
+ this.hoistedByText.set(text, name);
160
+ return name;
161
+ }
162
+
163
+ private range(range: SourceRange): string {
164
+ return this.hoist(`[${range[0]}, ${range[1]}]`);
165
+ }
166
+
167
+ /**
168
+ * The name a comprehension's element is bound to: a **parameter** of the body function,
169
+ * numbered per module so a nested body can never shadow the one around it.
170
+ */
171
+ private elementName(): string {
172
+ const name = `b${this.bindings}`;
173
+ this.bindings += 1;
174
+ return name;
175
+ }
176
+
177
+ // --- the tree -----------------------------------------------------------
178
+
179
+ private node(node: CelNode, scope: Scope | undefined, fn: Temporaries): string {
180
+ switch (node.kind) {
181
+ case "literal":
182
+ return this.literal(node.literal);
183
+ case "ident":
184
+ return this.ident(node, scope);
185
+ case "list":
186
+ return this.list(node, scope, fn);
187
+ case "map":
188
+ return this.map(node, scope, fn);
189
+ case "select":
190
+ return this.select(node, scope, fn);
191
+ case "index":
192
+ return this.index(node, scope, fn);
193
+ case "unary":
194
+ return this.call(node.operator, "global", [this.node(node.operand, scope, fn)], node.range, fn);
195
+ case "binary":
196
+ return this.binary(node, scope, fn);
197
+ case "conditional":
198
+ return this.conditional(node, scope, fn);
199
+ case "call":
200
+ case "receiverCall":
201
+ return this.anyCall(node, scope, fn);
202
+ case "qcall":
203
+ return this.qualifiedCall(node, scope, fn);
204
+ case "unparsed":
205
+ throw new CelCompileError("the expression could not be read whole, so it cannot be compiled");
206
+ }
207
+ }
208
+
209
+ /**
210
+ * A literal as the source that builds its value. A plain value is written inline; a uint
211
+ * and a bytes literal are **hoisted**, because each is an object and allocating one per
212
+ * evaluation would be work the closure backend does once at compile time.
213
+ */
214
+ private literal(literal: CelLiteral): string {
215
+ switch (literal.type) {
216
+ case "int":
217
+ return integerSource(literal.value);
218
+ case "uint":
219
+ return this.hoist(`celUint(${integerSource(literal.value)})`);
220
+ case "double":
221
+ return doubleSource(literal.value);
222
+ case "string":
223
+ return textSource(literal.value);
224
+ case "bytes":
225
+ return this.hoist(`new Uint8Array([${[...literal.value].join(", ")}])`);
226
+ case "bool":
227
+ return literal.value ? "true" : "false";
228
+ case "null":
229
+ return "null";
230
+ }
231
+ }
232
+
233
+ // --- names --------------------------------------------------------------
234
+
235
+ private ident(node: Extract<CelNode, { kind: "ident" }>, scope: Scope | undefined): string {
236
+ if (!node.absolute) {
237
+ const held = boundName(scope, node.name);
238
+ if (held !== undefined) return held;
239
+ }
240
+ return `readName(frame.activation, constants, ${textSource(node.name)}, ${this.range(node.range)})`;
241
+ }
242
+
243
+ /**
244
+ * A chain of plain member names rooted at a free name. The split over the names the host
245
+ * declared happens **here, at emit time**, through the same function the checker and the
246
+ * closure backend split it with — so the emitted code performs one activation read and
247
+ * then member reads, and can never read a different name than the one the check typed.
248
+ */
249
+ private chain(segments: readonly string[], range: SourceRange): string {
250
+ const declaredSplit = splitDeclaredChain(segments, this.target.declares);
251
+ const at = this.range(range);
252
+ if (declaredSplit) {
253
+ const rest = this.hoist(`[${declaredSplit.rest.map(textSource).join(", ")}]`);
254
+ return `readNameChain(frame.activation, constants, ${textSource(declaredSplit.name)}, ${rest}, ${at})`;
255
+ }
256
+ // Nothing declares a prefix, so the activation is searched longest prefix first — the
257
+ // same fallback, over the same candidate list, that the closure backend builds.
258
+ const candidates = this.hoist(
259
+ `[${prefixCandidates(segments)
260
+ .map(
261
+ (candidate) =>
262
+ `{ name: ${textSource(candidate.name)}, rest: [${candidate.rest.map(textSource).join(", ")}] }`,
263
+ )
264
+ .join(", ")}]`,
265
+ );
266
+ return `searchNameChain(frame.activation, constants, ${candidates}, ${textSource(segments.join("."))}, ${at})`;
267
+ }
268
+
269
+ // --- member reads -------------------------------------------------------
270
+
271
+ private select(node: CelSelectNode, scope: Scope | undefined, fn: Temporaries): string {
272
+ const chain = plainMemberChain(node, (name) => boundName(scope, name) !== undefined);
273
+ if (chain) return this.chain(chain, node.range);
274
+ const operand = this.node(node.operand, scope, fn);
275
+ const at = this.range(node.range);
276
+ return this.carrying(
277
+ [operand],
278
+ fn,
279
+ ([held]) => `readThrough(${held}, ${textSource(node.field)}, ${node.optional}, ${at})`,
280
+ );
281
+ }
282
+
283
+ private index(
284
+ node: Extract<CelNode, { kind: "index" }>,
285
+ scope: Scope | undefined,
286
+ fn: Temporaries,
287
+ ): string {
288
+ const operand = this.node(node.operand, scope, fn);
289
+ const key = this.node(node.index, scope, fn);
290
+ const at = this.range(node.range);
291
+ return this.carrying(
292
+ [operand, key],
293
+ fn,
294
+ ([held, named]) => `readThrough(${held}, ${named}, ${node.optional}, ${at})`,
295
+ );
296
+ }
297
+
298
+ // --- aggregates ---------------------------------------------------------
299
+
300
+ private list(
301
+ node: Extract<CelNode, { kind: "list" }>,
302
+ scope: Scope | undefined,
303
+ fn: Temporaries,
304
+ ): string {
305
+ const out = fn.next();
306
+ const at = this.range(node.range);
307
+ const entries = node.elements.map((element) => ({
308
+ value: this.node(element.value, scope, fn),
309
+ optional: element.optional,
310
+ }));
311
+ let body = out;
312
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
313
+ const entry = entries[index]!;
314
+ const value = fn.next();
315
+ if (!entry.optional) {
316
+ body = `(${value} = ${entry.value}, isCelError(${value}) ? ${value} : (${out}.push(${value}), ${body}))`;
317
+ continue;
318
+ }
319
+ // An absent optional entry leaves no element: the aggregate shrinks, which is the
320
+ // whole reason to write one.
321
+ const held = fn.next();
322
+ body =
323
+ `(${value} = ${entry.value}, isCelError(${value}) ? ${value} : ` +
324
+ `(${held} = optionalEntry(${value}, ${at}), isCelError(${held}) ? ${held} : ` +
325
+ `(${held}.present && ${out}.push(${held}.held), ${body})))`;
326
+ }
327
+ return `(${out} = [], ${body})`;
328
+ }
329
+
330
+ private map(
331
+ node: Extract<CelNode, { kind: "map" }>,
332
+ scope: Scope | undefined,
333
+ fn: Temporaries,
334
+ ): string {
335
+ const out = fn.next();
336
+ const at = this.range(node.range);
337
+ const entries = node.entries.map((entry) => ({
338
+ key: this.node(entry.key, scope, fn),
339
+ value: this.node(entry.value, scope, fn),
340
+ optional: entry.optional,
341
+ }));
342
+ let body = `celMapFromEntries(${out}, ${at})`;
343
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
344
+ const entry = entries[index]!;
345
+ const key = fn.next();
346
+ const value = fn.next();
347
+ const kept = entry.optional ? fn.next() : undefined;
348
+ const written = kept
349
+ ? `(${kept} = optionalEntry(${value}, ${at}), isCelError(${kept}) ? ${kept} : ` +
350
+ `(${kept}.present && ${out}.push([${key}, ${kept}.held]), ${body}))`
351
+ : `(${out}.push([${key}, ${value}]), ${body})`;
352
+ body =
353
+ `(${key} = ${entry.key}, isCelError(${key}) ? ${key} : ` +
354
+ `(${value} = ${entry.value}, isCelError(${value}) ? ${value} : ${written}))`;
355
+ }
356
+ return `(${out} = [], ${body})`;
357
+ }
358
+
359
+ // --- operators ----------------------------------------------------------
360
+
361
+ /**
362
+ * `&&` and `||` carry an error-valued operand through: `false && <error>` is `false` and
363
+ * `true || <error>` is `true`, whichever side the error is on. The emitted form is the
364
+ * closure backend's branch order written out, so an error on one side and a non-bool on
365
+ * the other decide the same way.
366
+ */
367
+ private binary(
368
+ node: Extract<CelNode, { kind: "binary" }>,
369
+ scope: Scope | undefined,
370
+ fn: Temporaries,
371
+ ): string {
372
+ if (node.operator !== "&&" && node.operator !== "||") {
373
+ return this.call(
374
+ node.operator,
375
+ "global",
376
+ [this.node(node.left, scope, fn), this.node(node.right, scope, fn)],
377
+ node.range,
378
+ fn,
379
+ );
380
+ }
381
+ const left = this.node(node.left, scope, fn);
382
+ const right = this.node(node.right, scope, fn);
383
+ const at = this.range(node.range);
384
+ const decided = node.operator === "&&" ? "false" : "true";
385
+ const undecided = node.operator === "&&" ? "true" : "false";
386
+ const a = fn.next();
387
+ const b = fn.next();
388
+ return (
389
+ `(${a} = boolOperand(${left}, ${at}), ${a} === ${decided} ? ${decided} : ` +
390
+ `(${b} = boolOperand(${right}, ${at}), ${b} === ${decided} ? ${decided} : ` +
391
+ `isCelError(${a}) ? ${a} : isCelError(${b}) ? ${b} : ${undecided}))`
392
+ );
393
+ }
394
+
395
+ private conditional(
396
+ node: Extract<CelNode, { kind: "conditional" }>,
397
+ scope: Scope | undefined,
398
+ fn: Temporaries,
399
+ ): string {
400
+ const condition = this.node(node.condition, scope, fn);
401
+ const whenTrue = this.node(node.whenTrue, scope, fn);
402
+ const whenFalse = this.node(node.whenFalse, scope, fn);
403
+ const at = this.range(node.range);
404
+ const held = fn.next();
405
+ return (
406
+ `(${held} = boolOperand(${condition}, ${at}), isCelError(${held}) ? ${held} : ` +
407
+ `${held} ? ${whenTrue} : ${whenFalse})`
408
+ );
409
+ }
410
+
411
+ // --- calls --------------------------------------------------------------
412
+
413
+ private anyCall(
414
+ node: Extract<CelNode, { kind: "call" | "receiverCall" }>,
415
+ scope: Scope | undefined,
416
+ fn: Temporaries,
417
+ ): string {
418
+ // The same question the checker asks, in the same place: a macro is not a function, and
419
+ // it is recognised before any overload is looked for.
420
+ if (isMacroCall(node)) return this.macro(node, scope, fn);
421
+ const args = [
422
+ ...(node.kind === "receiverCall" ? [this.node(node.receiver, scope, fn)] : []),
423
+ ...node.args.map((argument) => this.node(argument, scope, fn)),
424
+ ];
425
+ return this.call(node.name, node.kind === "call" ? "global" : "receiver", args, node.range, fn);
426
+ }
427
+
428
+ /**
429
+ * One dispatch: evaluate the arguments, carry the first error out, then hand the values to
430
+ * the site. The site is hoisted, so it is built once per loaded module and holds the
431
+ * overloads it resolved — exactly as a compiled closure's does.
432
+ */
433
+ private call(
434
+ name: string,
435
+ form: "global" | "receiver",
436
+ args: readonly string[],
437
+ range: SourceRange,
438
+ fn: Temporaries,
439
+ ): string {
440
+ const site = this.hoist(
441
+ `callSite(${textSource(name)}, ${textSource(form)}, ${this.range(range)})`,
442
+ );
443
+ return this.carrying(args, fn, (values) => `${site}.call([${values.join(", ")}])`);
444
+ }
445
+
446
+ private qualifiedCall(
447
+ node: Extract<CelNode, { kind: "qcall" }>,
448
+ scope: Scope | undefined,
449
+ fn: Temporaries,
450
+ ): string {
451
+ const args = node.args.map((argument) => this.node(argument, scope, fn));
452
+ const at = this.range(node.range);
453
+ const context = this.hoist(`{ range: ${at} }`);
454
+ const bound = fn.next();
455
+ // The arguments are evaluated only once something IS bound, which is the closure
456
+ // backend's order: a call nothing bound is `unbound_function` even where an argument
457
+ // would have failed.
458
+ const dispatched = this.carrying(
459
+ args,
460
+ fn,
461
+ (values) => `readHostValue(${bound}([${values.join(", ")}], ${context}), ${at})`,
462
+ );
463
+ const refusal = `celError("unbound_function", ${textSource(
464
+ `unbound function '${node.namespace}.${node.name}' — nothing is bound under that name here`,
465
+ )}, ${at})`;
466
+ return (
467
+ `(${bound} = frame.namespaceFunction ? frame.namespaceFunction(${textSource(node.namespace)}, ` +
468
+ `${textSource(node.name)}) : undefined, !${bound} ? ${refusal} : ${dispatched})`
469
+ );
470
+ }
471
+
472
+ // --- macros -------------------------------------------------------------
473
+
474
+ private macro(
475
+ node: Extract<CelNode, { kind: "call" | "receiverCall" }>,
476
+ scope: Scope | undefined,
477
+ fn: Temporaries,
478
+ ): string {
479
+ if (node.kind === "call") return this.has(node, scope, fn);
480
+ const receiver = node.receiver;
481
+ if (receiver.kind === "ident" && receiver.name === "cel" && node.name === "bind") {
482
+ return this.celBind(node, scope, fn);
483
+ }
484
+ if (receiver.kind === "ident" && receiver.name === "optional") {
485
+ return this.optionalNamespace(node, scope, fn);
486
+ }
487
+ if ((node.name === "optMap" || node.name === "optFlatMap") && node.args.length === 2) {
488
+ return this.optionalBinding(node, scope, fn);
489
+ }
490
+ return this.comprehension(node, scope, fn);
491
+ }
492
+
493
+ /** `has(a.b)` — presence, which a missing key answers `false` rather than erroring. */
494
+ private has(
495
+ node: Extract<CelNode, { kind: "call" }>,
496
+ scope: Scope | undefined,
497
+ fn: Temporaries,
498
+ ): string {
499
+ const argument = node.args[0]!;
500
+ if (argument.kind !== "select") {
501
+ throw new CelCompileError("has() asks about a member, so its argument ends in a select");
502
+ }
503
+ const operand = this.node(argument.operand, scope, fn);
504
+ const at = this.range(node.range);
505
+ return this.carrying(
506
+ [operand],
507
+ fn,
508
+ ([held]) => `hasMember(${held}, ${textSource(argument.field)}, ${at})`,
509
+ );
510
+ }
511
+
512
+ private celBind(
513
+ node: Extract<CelNode, { kind: "receiverCall" }>,
514
+ scope: Scope | undefined,
515
+ fn: Temporaries,
516
+ ): string {
517
+ const binding = namespaceMacroBinding("cel", node.name, node.args.length);
518
+ if (!binding) {
519
+ throw new CelCompileError("cel.bind(name, value, body) takes a name, its value and a body");
520
+ }
521
+ const name = node.args[binding.variableArgument]!;
522
+ if (name.kind !== "ident") throw new CelCompileError("cel.bind binds a name");
523
+ const value = this.node(node.args[1]!, scope, fn);
524
+ // The bound value lives in a temporary of the enclosing function, so it is DECLARED
525
+ // there: a body nested inside this one captures it as a closure captures any `let`.
526
+ const held = fn.next();
527
+ const body = this.node(node.args[2]!, { name: name.name, js: held, outer: scope }, fn);
528
+ const at = this.range(node.range);
529
+ const refused = fn.next();
530
+ // A name is bound to a VALUE, so a value that must be awaited is refused here rather
531
+ // than inside the body, where every read of the name would meet it again.
532
+ return (
533
+ `(${held} = ${value}, isCelError(${held}) ? ${held} : ` +
534
+ `(${refused} = asyncValueRefused(${held}, ${at}), ${refused} ? ${refused} : ${body}))`
535
+ );
536
+ }
537
+
538
+ /** `optional.of(v)`, `optional.ofNonZeroValue(v)` and `optional.none()`. */
539
+ private optionalNamespace(
540
+ node: Extract<CelNode, { kind: "receiverCall" }>,
541
+ scope: Scope | undefined,
542
+ fn: Temporaries,
543
+ ): string {
544
+ if (node.name === "none") return "none";
545
+ if (node.args.length !== 1) {
546
+ throw new CelCompileError(`optional.${node.name}(value) takes one argument`);
547
+ }
548
+ const value = this.node(node.args[0]!, scope, fn);
549
+ const wrap = node.name === "ofNonZeroValue" ? "optionalOfNonZero" : "celSome";
550
+ return this.carrying([value], fn, ([held]) => `${wrap}(${held})`);
551
+ }
552
+
553
+ /** `opt.optMap(v, body)` wraps what the body answers; `optFlatMap` does not. */
554
+ private optionalBinding(
555
+ node: Extract<CelNode, { kind: "receiverCall" }>,
556
+ scope: Scope | undefined,
557
+ fn: Temporaries,
558
+ ): string {
559
+ const name = node.args[0]!;
560
+ if (name.kind !== "ident") throw new CelCompileError(`${node.name} binds a name`);
561
+ const receiver = this.node(node.receiver, scope, fn);
562
+ const at = this.range(node.range);
563
+ const optional = fn.next();
564
+ const refused = fn.next();
565
+ const answered = fn.next();
566
+ const held = fn.next();
567
+ const body = this.node(node.args[1]!, { name: name.name, js: held, outer: scope }, fn);
568
+ const wrong = `celError("no_matching_overload", ${textSource(`${JSON.stringify(node.name)} reads an optional`)}, ${at})`;
569
+ const result =
570
+ node.name === "optMap"
571
+ ? `celSome(${answered})`
572
+ : `isCelOptional(${answered}) ? ${answered} : celError("no_matching_overload", ` +
573
+ `${textSource("optFlatMap's body answers an optional")}, ${at})`;
574
+ // A host may hand over the optional itself, so what it HOLDS is a door of its own.
575
+ return (
576
+ `(${optional} = ${receiver}, isCelError(${optional}) ? ${optional} : ` +
577
+ `!isCelOptional(${optional}) ? ${wrong} : ` +
578
+ `!${optional}.present ? none : ` +
579
+ `(${refused} = asyncValueRefused(${optional}.held, ${at}), ${refused} ? ${refused} : ` +
580
+ `(${held} = ${optional}.held, ${answered} = ${body}, isCelError(${answered}) ? ${answered} : ${result})))`
581
+ );
582
+ }
583
+
584
+ /**
585
+ * A comprehension. The body becomes a function of the element, exactly as the closure
586
+ * backend passes one, so the macro's meaning — including which error outranks which
587
+ * decided answer — stays `comprehension-runtime.ts`'s alone.
588
+ */
589
+ private comprehension(
590
+ node: Extract<CelNode, { kind: "receiverCall" }>,
591
+ scope: Scope | undefined,
592
+ fn: Temporaries,
593
+ ): string {
594
+ const binding = receiverMacroBinding(node.name, node.args.length);
595
+ if (!binding) throw new CelCompileError(`${node.name} is not a comprehension`);
596
+ const name = node.args[binding.variableArgument]!;
597
+ if (name.kind !== "ident") throw new CelCompileError(`${node.name} binds a name`);
598
+ const receiver = this.node(node.receiver, scope, fn);
599
+ const at = this.range(node.range);
600
+ const element = this.elementName();
601
+ const inner: Scope = { name: name.name, js: element, outer: scope };
602
+ const bodies = binding.scopedArguments.map((argument) => this.body(node.args[argument]!, inner, element));
603
+ const elements = fn.next();
604
+ const call = ((): string => {
605
+ switch (node.name) {
606
+ case "all":
607
+ return `celAll(${elements}, ${bodies[0]}, ${at})`;
608
+ case "exists":
609
+ return `celExists(${elements}, ${bodies[0]}, ${at})`;
610
+ case "exists_one":
611
+ return `celExistsOne(${elements}, ${bodies[0]}, ${at})`;
612
+ case "filter":
613
+ return `celFilter(${elements}, ${bodies[0]}, ${at})`;
614
+ default:
615
+ return bodies.length === 2
616
+ ? `celMapComprehension(${elements}, ${bodies[1]}, ${bodies[0]}, ${at})`
617
+ : `celMapComprehension(${elements}, ${bodies[0]}, undefined, ${at})`;
618
+ }
619
+ })();
620
+ return this.carrying(
621
+ [receiver],
622
+ fn,
623
+ ([held]) =>
624
+ `(${elements} = celIterable(${held}, ${at}), isCelError(${elements}) ? ${elements} : ${call})`,
625
+ );
626
+ }
627
+
628
+ /** A body a comprehension calls per element: its own function, with its own temporaries. */
629
+ private body(node: CelNode, scope: Scope, parameter: string): string {
630
+ const temporaries = this.temporaries();
631
+ const text = this.node(node, scope, temporaries);
632
+ return `(${parameter}) => { ${temporaries.declaration()}return ${text}; }`;
633
+ }
634
+
635
+ // --- the one sequencing rule --------------------------------------------
636
+
637
+ /**
638
+ * Evaluate each expression in order and carry the first error out, then build the answer
639
+ * from the values. Every form that evaluates more than one thing goes through here, so a
640
+ * later operand is never evaluated after an earlier one failed — which is what makes an
641
+ * error a value that short-circuits rather than an exception.
642
+ */
643
+ private carrying(
644
+ expressions: readonly string[],
645
+ fn: Temporaries,
646
+ answer: (values: readonly string[]) => string,
647
+ ): string {
648
+ const values = expressions.map(() => fn.next());
649
+ let out = answer(values);
650
+ for (let at = expressions.length - 1; at >= 0; at -= 1) {
651
+ out = `(${values[at]} = ${expressions[at]}, isCelError(${values[at]}) ? ${values[at]} : ${out})`;
652
+ }
653
+ return out;
654
+ }
655
+ }
656
+
657
+ // --- writing a value as source ---------------------------------------------
658
+
659
+ /** An int or uint literal as a `bigint`, parenthesized where it is negative. */
660
+ function integerSource(value: bigint): string {
661
+ return value < 0n ? `(${value}n)` : `${value}n`;
662
+ }
663
+
664
+ /**
665
+ * A double as source. `-0` is a value CEL tells apart from `0`, and an overflowing literal
666
+ * reads as an infinity, so neither may go through the shortest decimal form.
667
+ */
668
+ function doubleSource(value: number): string {
669
+ if (Number.isNaN(value)) return "NaN";
670
+ if (value === Number.POSITIVE_INFINITY) return "Infinity";
671
+ if (value === Number.NEGATIVE_INFINITY) return "(-Infinity)";
672
+ if (Object.is(value, -0)) return "(-0)";
673
+ return value < 0 ? `(${value})` : `${value}`;
674
+ }
675
+
676
+ const ESCAPES: Readonly<Record<string, string>> = {
677
+ "\\": "\\\\",
678
+ '"': '\\"',
679
+ "\n": "\\n",
680
+ "\r": "\\r",
681
+ "\t": "\\t",
682
+ "\b": "\\b",
683
+ "\f": "\\f",
684
+ "\v": "\\v",
685
+ };
686
+
687
+ /**
688
+ * Text as a JavaScript string literal. Every character outside printable ASCII is written
689
+ * as an escape — a lone surrogate, a line separator and a zero-width joiner all read back
690
+ * as themselves, and the emitted module is pure ASCII however its expressions were written,
691
+ * so no encoding assumption about the file it is stored in can change its meaning.
692
+ */
693
+ export function textSource(text: string): string {
694
+ let out = '"';
695
+ for (const unit of text) {
696
+ const held = ESCAPES[unit];
697
+ if (held !== undefined) {
698
+ out += held;
699
+ continue;
700
+ }
701
+ const code = unit.codePointAt(0)!;
702
+ if (code >= 0x20 && code <= 0x7e) {
703
+ out += unit;
704
+ continue;
705
+ }
706
+ if (code > 0xffff) {
707
+ out += `\\u{${code.toString(16)}}`;
708
+ continue;
709
+ }
710
+ out += `\\u${code.toString(16).padStart(4, "0")}`;
711
+ }
712
+ return `${out}"`;
713
+ }