@telorun/cel 0.0.0-stage → 0.108.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 +184 -0
  7. package/dist/backend-runtime.d.ts.map +1 -0
  8. package/dist/backend-runtime.js +425 -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 +786 -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 +47 -0
  19. package/dist/cel-map-value.d.ts.map +1 -0
  20. package/dist/cel-map-value.js +85 -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 +166 -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 +54 -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 +86 -0
  34. package/dist/checker.d.ts.map +1 -0
  35. package/dist/checker.js +806 -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 +487 -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 +70 -0
  49. package/dist/duration-value.d.ts.map +1 -0
  50. package/dist/duration-value.js +149 -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 +291 -0
  61. package/dist/environment.d.ts.map +1 -0
  62. package/dist/environment.js +474 -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 +118 -0
  67. package/dist/function-registry.d.ts.map +1 -0
  68. package/dist/function-registry.js +292 -0
  69. package/dist/index.d.ts +93 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +65 -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 +133 -0
  76. package/dist/js-emitter.d.ts.map +1 -0
  77. package/dist/js-emitter.js +568 -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 +41 -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 +89 -0
  94. package/dist/member-read.d.ts.map +1 -0
  95. package/dist/member-read.js +166 -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 +46 -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 +67 -0
  127. package/dist/runtime-library.d.ts.map +1 -0
  128. package/dist/runtime-library.js +554 -0
  129. package/dist/serializer.d.ts +24 -0
  130. package/dist/serializer.d.ts.map +1 -0
  131. package/dist/serializer.js +256 -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 +62 -0
  150. package/dist/timestamp-value.d.ts.map +1 -0
  151. package/dist/timestamp-value.js +238 -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 +42 -0
  156. package/dist/type-expression.d.ts.map +1 -0
  157. package/dist/type-expression.js +154 -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 +604 -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 +97 -0
  174. package/src/cel-program.ts +103 -0
  175. package/src/cel-type.ts +359 -0
  176. package/src/cel-value.ts +361 -0
  177. package/src/check-diagnostic.ts +104 -0
  178. package/src/checker.ts +1045 -0
  179. package/src/closure-backend.ts +547 -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 +160 -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 +761 -0
  188. package/src/function-catalog.ts +140 -0
  189. package/src/function-registry.ts +341 -0
  190. package/src/index.ts +407 -0
  191. package/src/integer-arithmetic.ts +64 -0
  192. package/src/js-emitter.ts +721 -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 +197 -0
  197. package/src/macro-shape.ts +66 -0
  198. package/src/member-read.ts +167 -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 +47 -0
  208. package/src/root-references.ts +126 -0
  209. package/src/runtime-library.ts +639 -0
  210. package/src/serializer.ts +262 -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 +310 -0
  219. package/src/tree-equality.ts +130 -0
  220. package/src/type-expression.ts +182 -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,806 @@
1
+ /**
2
+ * The type checker: one walk, every verdict.
3
+ *
4
+ * It answers three things at once, because all three come from the same walk and
5
+ * nothing else can derive them: the type of the expression, every mistake in it with a
6
+ * range and a code, and which signature each call resolved to.
7
+ *
8
+ * Two properties are deliberate and worth keeping:
9
+ *
10
+ * - **One mistake is one diagnostic.** A subexpression that failed is typed `dyn`, so
11
+ * the operator above it does not report a second mistake about the first one. An
12
+ * editor shows what is wrong, not what is downstream of it.
13
+ * - **Nothing reads a message.** Every code is decided where the cause is known — an
14
+ * unknown name, a call written in the other form, a type argument that differs — and
15
+ * a repair, where there is an obvious one, is built by rewriting the tree and writing
16
+ * it back out, so a fix is always a whole source that parses.
17
+ */
18
+ import { admitsNull, assignable, BOOL, DYN, DOUBLE, BYTES, formatType, INT, isDyn, listOf, mapOf, NULL, optionalOf, parameterOf, STRING, UINT, unify, unionOf, withoutNull, withoutParameters, } from "./cel-type.js";
19
+ import { literalValue } from "./cel-value.js";
20
+ import { splitDeclaredChain } from "./declared-chain.js";
21
+ import { DiagnosticList } from "./check-diagnostic.js";
22
+ import { describeArguments, describeCandidates } from "./function-registry.js";
23
+ import { checkMacro, isMacroCall } from "./macro-check.js";
24
+ import { macroShapeFindings } from "./macro-shape.js";
25
+ import { chainRoot, chainText, provenWhereFalse, provenWhereTrue } from "./nullable-access.js";
26
+ import { serializeTree } from "./serializer.js";
27
+ import { formatSignature } from "./signature.js";
28
+ export function checkExpression(expression, context) {
29
+ const checker = new Checker(expression, context);
30
+ return checker.run();
31
+ }
32
+ class Checker {
33
+ expression;
34
+ context;
35
+ diagnostics = new DiagnosticList();
36
+ calls = [];
37
+ scopes = [];
38
+ proven = new Set();
39
+ constructor(expression, context) {
40
+ this.expression = expression;
41
+ this.context = context;
42
+ }
43
+ run() {
44
+ for (const syntax of this.expression.diagnostics) {
45
+ this.diagnostics.add({ code: "CEL_SYNTAX_ERROR", message: syntax.message, range: syntax.range });
46
+ }
47
+ for (const finding of macroShapeFindings(this.expression.root)) {
48
+ this.report(finding.code, finding.message, finding.range);
49
+ }
50
+ // The type is REPORTED, so an unresolved parameter in it is `dyn`: that is cel-spec's
51
+ // own rule for a parameter nothing resolved, and it is the same rule this checker
52
+ // already applies to every use of one inside an expression. A parameter survives in a
53
+ // declaration — a signature, a nominal type's parameter list — and nowhere else.
54
+ const type = withoutParameters(this.typeOf(this.expression.root));
55
+ return {
56
+ valid: this.diagnostics.length === 0,
57
+ type,
58
+ typeName: formatType(type, true),
59
+ diagnostics: this.diagnostics.list(),
60
+ calls: this.calls,
61
+ };
62
+ }
63
+ // --- MacroHost ----------------------------------------------------------
64
+ report(code, message, range, fix) {
65
+ this.diagnostics.add({ code, message, range, ...(fix ? { fix } : {}) });
66
+ }
67
+ typeOfBinding(node, bindings) {
68
+ this.scopes.push(new Map(bindings));
69
+ try {
70
+ return this.typeOf(node);
71
+ }
72
+ finally {
73
+ this.scopes.pop();
74
+ }
75
+ }
76
+ typeOfPresence(node) {
77
+ return this.selectType(node, true);
78
+ }
79
+ // --- the walk -----------------------------------------------------------
80
+ typeOf(node) {
81
+ switch (node.kind) {
82
+ case "unparsed":
83
+ return DYN;
84
+ case "literal":
85
+ return this.literalType(node.literal.type);
86
+ case "ident":
87
+ return this.identType(node);
88
+ case "list":
89
+ return this.listType(node);
90
+ case "map":
91
+ return this.mapType(node);
92
+ case "select":
93
+ return this.selectType(node, node.optional);
94
+ case "index":
95
+ return this.indexType(node);
96
+ case "unary":
97
+ return this.operatorType(node.operator, [node.operand], node.range);
98
+ case "binary":
99
+ return this.binaryType(node);
100
+ case "conditional":
101
+ return this.conditionalType(node);
102
+ case "call":
103
+ case "receiverCall":
104
+ return this.callType(node);
105
+ case "qcall":
106
+ return this.qualifiedCallType(node);
107
+ }
108
+ }
109
+ literalType(type) {
110
+ switch (type) {
111
+ case "int":
112
+ return INT;
113
+ case "uint":
114
+ return UINT;
115
+ case "double":
116
+ return DOUBLE;
117
+ case "string":
118
+ return STRING;
119
+ case "bytes":
120
+ return BYTES;
121
+ case "bool":
122
+ return BOOL;
123
+ default:
124
+ return NULL;
125
+ }
126
+ }
127
+ identType(node) {
128
+ // An absolute name is resolved against the environment alone: that is what it is for.
129
+ if (!node.absolute) {
130
+ for (let at = this.scopes.length - 1; at >= 0; at -= 1) {
131
+ const bound = this.scopes[at].get(node.name);
132
+ if (bound)
133
+ return bound;
134
+ }
135
+ }
136
+ const declared = this.context.variable(node.name);
137
+ if (declared)
138
+ return declared;
139
+ if (this.context.options.unlistedVariablesAreDyn)
140
+ return DYN;
141
+ this.report("CEL_UNKNOWN_IDENTIFIER", `${JSON.stringify(node.name)} is not declared here${this.availableNames()}`, node.range);
142
+ return DYN;
143
+ }
144
+ availableNames() {
145
+ const names = this.context.declaredVariableNames();
146
+ return names.length === 0 ? "" : ` (declared: ${[...names].sort().join(", ")})`;
147
+ }
148
+ listType(node) {
149
+ if (node.elements.length === 0)
150
+ return listOf(parameterOf("T"));
151
+ const types = node.elements.map((element) => element.optional
152
+ ? this.heldByOptional(this.typeOf(element.value), element.value, "a list element")
153
+ : this.typeOf(element.value));
154
+ return listOf(this.reduce(types, node.elements.map((element) => element.value), "list"));
155
+ }
156
+ /**
157
+ * What an optional entry contributes: the value inside the optional. The entry holds an
158
+ * `optional<T>` and the aggregate holds `T`, because an absent one leaves no entry at
159
+ * all — which is the whole point of writing it that way.
160
+ */
161
+ heldByOptional(type, node, what) {
162
+ if (type.kind === "optional")
163
+ return type.value;
164
+ if (isDyn(type) || type.kind === "parameter")
165
+ return DYN;
166
+ this.report("CEL_TYPE_ERROR", `${what} written with '?' holds an optional, and this one is ${formatType(type)}`, node.range);
167
+ return DYN;
168
+ }
169
+ mapType(node) {
170
+ if (node.entries.length === 0)
171
+ return mapOf(parameterOf("K"), parameterOf("V"));
172
+ const keys = node.entries.map((entry) => this.typeOf(entry.key));
173
+ const values = node.entries.map((entry) => entry.optional
174
+ ? this.heldByOptional(this.typeOf(entry.value), entry.value, "a map entry")
175
+ : this.typeOf(entry.value));
176
+ return mapOf(this.reduce(keys, node.entries.map((entry) => entry.key), "map key"), this.reduce(values, node.entries.map((entry) => entry.value), "map value"));
177
+ }
178
+ /**
179
+ * One type for several written values. Mixed types are `dyn` — CEL's own reading of a
180
+ * heterogeneous literal — unless the environment asked for homogeneous aggregates, in
181
+ * which case the mixture is the mistake.
182
+ */
183
+ reduce(types, nodes, what) {
184
+ let reduced = types[0];
185
+ for (let at = 1; at < types.length; at += 1) {
186
+ const next = unify(reduced, types[at]);
187
+ if (this.context.options.homogeneousAggregateLiterals &&
188
+ isDyn(next) &&
189
+ !isDyn(reduced) &&
190
+ !isDyn(types[at])) {
191
+ this.report("CEL_TYPE_ERROR", `this ${what} is ${formatType(types[at])} where the others are ${formatType(reduced)}`, nodes[at].range);
192
+ }
193
+ reduced = next;
194
+ }
195
+ return reduced;
196
+ }
197
+ /**
198
+ * `presence` is whether the read asks whether the member is THERE — written `.?b`, or
199
+ * standing as the argument of `has()`. It decides nothing about a type that is known to
200
+ * hold no members, which both forms refuse; it decides what a UNION's member-less
201
+ * branches are, which is the absence case a presence-shaped read answers for.
202
+ */
203
+ selectType(node, presence) {
204
+ const qualified = this.qualifiedVariableType(node);
205
+ if (qualified)
206
+ return qualified;
207
+ const operand = this.typeOf(node.operand);
208
+ if (node.field === "")
209
+ return DYN;
210
+ const guarded = this.checkNullable(node.operand, operand, `.${node.field}`, node.fieldRange);
211
+ const held = this.memberType(guarded, node.field, node.fieldRange, presence);
212
+ // Reading through an optional answers an optional, whichever form the read is
213
+ // written in: that is what makes a chain of reads over a value that may be absent
214
+ // stay one expression instead of needing a guard at every step.
215
+ return node.optional || guarded.kind === "optional" ? optionalOf(unwrapOptional(held)) : held;
216
+ }
217
+ /**
218
+ * A dotted declaration is **one name**, and the longest one wins.
219
+ *
220
+ * A host may declare `a.b.c`, or `a.b` holding a map, or both. `a.b.c` then reads the
221
+ * variable of that name where it is declared, and the map's entry where only `a.b` is —
222
+ * so the host's own naming decides, not the shape of the expression. A name something
223
+ * inside the expression bound takes precedence over a declaration, as a bare name does,
224
+ * unless the chain is absolute.
225
+ */
226
+ qualifiedVariableType(node) {
227
+ const chain = plainChain(node);
228
+ if (!chain)
229
+ return undefined;
230
+ if (!chain.root.absolute && this.isBound(chain.root.name))
231
+ return undefined;
232
+ // The one split, shared with the backend (`declared-chain.ts`): two readers of "which
233
+ // name is this chain" would let the checker type one name while evaluation reads another.
234
+ const segments = chain.segments.map((segment) => segment.name);
235
+ const split = splitDeclaredChain(segments, (name) => this.context.variable(name) !== undefined);
236
+ if (!split)
237
+ return undefined;
238
+ // A split at the ROOT alone is the ordinary read of a name and then its members, which the
239
+ // select walk beside this types already — including the nullable-access rule, which is a
240
+ // fact about the expression's shape rather than about which name it reads. Only a DOTTED
241
+ // declaration is this question; the backend uses the whole answer, so neither searches a
242
+ // prefix the other declared.
243
+ if (split.rest.length === segments.length - 1)
244
+ return undefined;
245
+ let held = this.context.variable(split.name);
246
+ for (const segment of chain.segments.slice(chain.segments.length - split.rest.length)) {
247
+ held = this.memberType(held, segment.name, segment.range);
248
+ }
249
+ return held;
250
+ }
251
+ isBound(name) {
252
+ return this.scopes.some((scope) => scope.has(name));
253
+ }
254
+ /** The type of a named member, reported against whatever the operand turned out to be. */
255
+ memberType(operand, field, range, presence = false) {
256
+ if (isDyn(operand) || operand.kind === "parameter")
257
+ return DYN;
258
+ if (operand.kind === "optional") {
259
+ return this.memberType(operand.value, field, range, presence);
260
+ }
261
+ if (operand.kind === "union") {
262
+ return unionOf(presenceBranches(operand, holdsMembers, presence).map((member) => this.memberType(member, field, range, presence)));
263
+ }
264
+ if (operand.kind === "record")
265
+ return this.recordMemberType(operand, field, range);
266
+ if (operand.kind === "map") {
267
+ if (!assignable(STRING, operand.key)) {
268
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} is keyed by ${formatType(operand.key)}, so ${JSON.stringify(field)} cannot name an entry of it`, range);
269
+ return DYN;
270
+ }
271
+ return operand.value;
272
+ }
273
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} holds no members`, range);
274
+ return DYN;
275
+ }
276
+ recordMemberType(record, field, range) {
277
+ const held = record.fields.get(field);
278
+ if (held)
279
+ return held;
280
+ if (record.open)
281
+ return DYN;
282
+ const declared = [...record.fields.keys()];
283
+ this.report("CEL_UNKNOWN_FIELD", `${JSON.stringify(field)} is not declared here${declared.length === 0 ? "" : ` (declared: ${declared.join(", ")})`}`, range);
284
+ return DYN;
285
+ }
286
+ indexType(node) {
287
+ const operand = this.typeOf(node.operand);
288
+ const index = this.typeOf(node.index);
289
+ const guarded = this.checkNullable(node.operand, operand, "[…]", node.range);
290
+ const held = this.elementType(guarded, index, node, node.optional);
291
+ return node.optional || guarded.kind === "optional" ? optionalOf(unwrapOptional(held)) : held;
292
+ }
293
+ elementType(operand, index, node, presence = false) {
294
+ if (isDyn(operand) || operand.kind === "parameter")
295
+ return DYN;
296
+ if (operand.kind === "optional") {
297
+ return this.elementType(operand.value, index, node, presence);
298
+ }
299
+ if (operand.kind === "union") {
300
+ return unionOf(presenceBranches(operand, holdsElements, presence).map((member) => this.elementType(member, index, node, presence)));
301
+ }
302
+ if (operand.kind === "list") {
303
+ if (!assignable(index, INT) && !isDyn(index)) {
304
+ this.report("CEL_TYPE_ERROR", `a list is indexed by int, and this index is ${formatType(index)}`, node.index.range);
305
+ }
306
+ return operand.element;
307
+ }
308
+ if (operand.kind === "map") {
309
+ if (!assignable(index, operand.key) && !isDyn(index)) {
310
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} is indexed by ${formatType(operand.key)}, and this index is ${formatType(index)}`, node.index.range);
311
+ }
312
+ return operand.value;
313
+ }
314
+ if (operand.kind === "record") {
315
+ const literal = node.index.kind === "literal" && node.index.literal.type === "string";
316
+ if (literal) {
317
+ return this.recordMemberType(operand, node.index.literal.value, node.index.range);
318
+ }
319
+ return DYN;
320
+ }
321
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} holds no elements`, node.range);
322
+ return DYN;
323
+ }
324
+ binaryType(node) {
325
+ if (node.operator === "&&" || node.operator === "||") {
326
+ const left = this.typeOf(node.left);
327
+ const proofs = node.operator === "&&" ? provenWhereTrue(node.left) : provenWhereFalse(node.left);
328
+ const right = this.withProven(proofs, () => this.typeOf(node.right));
329
+ for (const type of [left, right]) {
330
+ if (!isDyn(type) && !assignable(type, BOOL)) {
331
+ this.report("CEL_TYPE_ERROR", `${node.operator} joins bool values, and ${formatType(type)} is not one`, node.range);
332
+ }
333
+ }
334
+ return BOOL;
335
+ }
336
+ return this.operatorType(node.operator, [node.left, node.right], node.range);
337
+ }
338
+ conditionalType(node) {
339
+ const condition = this.typeOf(node.condition);
340
+ if (!isDyn(condition) && !assignable(condition, BOOL)) {
341
+ this.report("CEL_TYPE_ERROR", `a condition must be bool, and this one is ${formatType(condition)}`, node.condition.range);
342
+ }
343
+ const whenTrue = this.withProven(provenWhereTrue(node.condition), () => this.typeOf(node.whenTrue));
344
+ const whenFalse = this.withProven(provenWhereFalse(node.condition), () => this.typeOf(node.whenFalse));
345
+ const unified = unify(whenTrue, whenFalse);
346
+ // A value read out of a conditional has one type, so two branches that share none
347
+ // is a mistake rather than a `dyn`: whoever reads it would be reading two things.
348
+ if (isDyn(unified) && !isDyn(whenTrue) && !isDyn(whenFalse)) {
349
+ this.report("CEL_TYPE_ERROR", `the branches of a condition answer ${formatType(whenTrue)} and ${formatType(whenFalse)}, which are not one type`, node.range);
350
+ }
351
+ return unified;
352
+ }
353
+ withProven(proofs, body) {
354
+ if (proofs.length === 0)
355
+ return body();
356
+ const outer = this.proven;
357
+ this.proven = new Set([...outer, ...proofs]);
358
+ try {
359
+ return body();
360
+ }
361
+ finally {
362
+ this.proven = outer;
363
+ }
364
+ }
365
+ /**
366
+ * A dereference of something that may be null. The type is answered with null taken
367
+ * out either way, so one missing guard is one diagnostic rather than a cascade.
368
+ */
369
+ checkNullable(operand, type, access, range) {
370
+ if (!admitsNull(type))
371
+ return type;
372
+ const chain = chainText(operand);
373
+ if (chain === undefined || this.proven.has(chain))
374
+ return withoutNull(type);
375
+ // **A chain rooted at a name the expression BOUND is not a subject of this verdict.** The
376
+ // guard constructs are only half the rule: the walk this replaces skipped such a chain
377
+ // entirely, so `xs.map(e, e.code)` over a nullable element is accepted today, and
378
+ // reporting it after the swap would newly reject a manifest — which is exactly what the
379
+ // guard set is held to its three forms to prevent.
380
+ const root = chainRoot(operand);
381
+ if (root !== undefined && this.isBound(root))
382
+ return withoutNull(type);
383
+ if (type.kind === "primitive") {
384
+ this.report("CEL_TYPE_ERROR", `null holds no members`, range);
385
+ return DYN;
386
+ }
387
+ this.report("CEL_NULLABLE_ACCESS", `${JSON.stringify(chain)} may be null — guard it (e.g. '${chain} != null && …' or '${chain} == null ? … : ${chain}${access}') before reading ${access}`, range);
388
+ return withoutNull(type);
389
+ }
390
+ operatorType(operator, operands, range) {
391
+ const args = operands.map((operand) => this.typeOf(operand));
392
+ // A value whose declared type admits null may always be tested against null —
393
+ // otherwise declaring it nullable would make it untestable, and the guard that
394
+ // clears a nullable read would itself be a type error.
395
+ if ((operator === "==" || operator === "!=") && isNullTest(args))
396
+ return BOOL;
397
+ const resolution = this.context.registry.resolve(operator, "global", args);
398
+ if ("resolved" in resolution)
399
+ return resolution.returns;
400
+ const mismatch = this.typeArgumentMismatch([...args, ...this.candidateTypes(resolution)]);
401
+ this.report(mismatch ? "CEL_TYPE_ARGUMENT_MISMATCH" : "CEL_TYPE_ERROR", mismatch ?? `no ${JSON.stringify(operator)} is declared over ${describeArguments(args)}`, range);
402
+ return DYN;
403
+ }
404
+ /**
405
+ * Two named types of one name whose arguments differ: invariance, so a mistake of its
406
+ * own rather than a plain type error. Only fully resolved types are compared — a
407
+ * signature's own parameter says nothing about what the call wanted.
408
+ */
409
+ typeArgumentMismatch(types) {
410
+ const nominals = types.filter((type) => type.kind === "nominal" && type.args.every((argument) => argument.kind !== "parameter"));
411
+ for (const [at, left] of nominals.entries()) {
412
+ for (const right of nominals.slice(at + 1)) {
413
+ if (left.name === right.name && !sameArguments(left.args, right.args)) {
414
+ return `${formatType(left)} and ${formatType(right)} are the same type with different type arguments, which never match`;
415
+ }
416
+ }
417
+ }
418
+ return undefined;
419
+ }
420
+ /** Every type a set of candidates mentions, for naming a type-argument mismatch. */
421
+ candidateTypes(failure) {
422
+ return failure.candidates.flatMap((candidate) => [
423
+ ...(candidate.signature.receiver ? [candidate.signature.receiver] : []),
424
+ ...candidate.signature.parameters,
425
+ ]);
426
+ }
427
+ callType(node) {
428
+ if (isMacroCall(node)) {
429
+ if (!this.context.options.enableOptionalTypes && isOptionalMacro(node)) {
430
+ this.report("CEL_UNKNOWN_FUNCTION", "optional types are not enabled in this environment", node.range);
431
+ return DYN;
432
+ }
433
+ return checkMacro(node, this);
434
+ }
435
+ const form = node.kind === "call" ? "global" : "receiver";
436
+ const receiver = node.kind === "receiverCall" ? this.typeOf(node.receiver) : undefined;
437
+ const args = node.args.map((argument) => this.typeOf(argument));
438
+ const resolution = this.context.registry.resolve(node.name, form, args, receiver);
439
+ if ("resolved" in resolution) {
440
+ this.calls.push(this.dispatched(node, form, args.length, resolution.resolved, resolution.returns));
441
+ this.checkLiteralArguments(node, resolution.resolved);
442
+ return resolution.returns;
443
+ }
444
+ this.calls.push({ name: node.name, form, arity: args.length, range: node.range });
445
+ this.reportCallFailure(node, form, args, receiver, resolution);
446
+ return DYN;
447
+ }
448
+ /**
449
+ * A registration's own guard over the arguments written as literals.
450
+ *
451
+ * It is asked here, where the call has just resolved, and nowhere else: a refusal over a
452
+ * VALUE is a verdict about the expression, so the component that decides every other
453
+ * verdict decides this one too — an implementation that refused the same argument at
454
+ * evaluation would leave a defect the source states behind a run. The refusal names the
455
+ * call as it was written, which is the one thing a reader needs to find it.
456
+ */
457
+ checkLiteralArguments(node, resolved) {
458
+ const check = resolved.metadata.checkArguments;
459
+ if (check === undefined)
460
+ return;
461
+ const written = node.kind === "receiverCall" ? [node.receiver, ...node.args] : node.args;
462
+ const refusal = check(written.map((argument) => (argument.kind === "literal" ? literalValue(argument.literal) : undefined)));
463
+ if (refusal === undefined)
464
+ return;
465
+ const source = this.expression.source.slice(node.range[0], node.range[1]);
466
+ this.report("CEL_INVALID_ARGUMENT", `${refusal} (in \`${source}\`)`, node.range);
467
+ }
468
+ dispatched(node, form, arity, resolved, returns) {
469
+ const { metadata } = resolved;
470
+ return {
471
+ name: node.name,
472
+ form,
473
+ arity,
474
+ range: node.range,
475
+ signature: formatSignature(resolved.signature),
476
+ returns: formatType(withoutParameters(returns)),
477
+ deterministic: metadata.deterministic ?? true,
478
+ hostBacked: metadata.hostBacked ?? false,
479
+ ...(metadata.throws ? { throws: metadata.throws } : {}),
480
+ };
481
+ }
482
+ reportCallFailure(node, form, args, receiver, failure) {
483
+ const written = `${node.name}(${describeArguments(args)})`;
484
+ if (failure.reason === "unknown") {
485
+ this.report("CEL_UNKNOWN_FUNCTION", `no function named ${JSON.stringify(node.name)} is registered${this.closestNames(node.name, form, args.length)}`, node.range, this.renameFix(node));
486
+ return;
487
+ }
488
+ if (failure.reason === "wrong-form") {
489
+ const wanted = form === "global" ? "on a value" : "without a receiver";
490
+ this.report("CEL_WRONG_CALL_FORM", `${JSON.stringify(node.name)} is called ${wanted}: ${describeCandidates(failure.candidates)}`, node.range, this.formFix(node));
491
+ return;
492
+ }
493
+ const mismatch = this.typeArgumentMismatch([
494
+ ...(receiver ? [receiver] : []),
495
+ ...args,
496
+ ...this.candidateTypes(failure),
497
+ ]);
498
+ this.report(mismatch ? "CEL_TYPE_ARGUMENT_MISMATCH" : "CEL_TYPE_ERROR", mismatch ??
499
+ `no overload of ${JSON.stringify(node.name)} takes ${form === "receiver" ? `${formatType(receiver ?? DYN)}.` : ""}${written}: ${describeCandidates(failure.candidates)}`, node.range);
500
+ }
501
+ /**
502
+ * What a call on an unregistered name is offered instead: the registered names that
503
+ * accept its form and arity, nearest first, as a clause to hang off the refusal.
504
+ *
505
+ * **The alternatives belong in the MESSAGE, and the fix stays singular.** A reader of
506
+ * `no('x')` is one name away from the five functions that would have worked, and the
507
+ * engine this one replaces said so; losing it is a real loss of help. A list of five
508
+ * repairs, though, is not a repair — an editor applying one would be guessing which
509
+ * function the author meant — so the single rename fix is still offered only where
510
+ * exactly one name is a case-insensitive match.
511
+ *
512
+ * It names no command and no tool: which listing a host offers is the host's, and an
513
+ * engine embedded in one with no CLI would be pointing at nothing.
514
+ */
515
+ closestNames(name, form, arity) {
516
+ const candidates = this.context.registry.candidateNames(name, form, arity);
517
+ if (candidates.length === 0)
518
+ return "";
519
+ const written = arity === 0 ? "no arguments" : arity === 1 ? "1 argument" : `${arity} arguments`;
520
+ const shape = form === "receiver" ? `written on a value with ${written}` : `taking ${written}`;
521
+ return ` — the closest ${shape}: ${candidates.join(", ")}`;
522
+ }
523
+ /** The one registered name this call might have meant, as a whole-source repair. */
524
+ renameFix(node) {
525
+ const wanted = node.name.toLowerCase();
526
+ const form = node.kind === "call" ? "global" : "receiver";
527
+ const candidates = new Set(this.context.registry
528
+ .list()
529
+ .filter((entry) => entry.signature.form === form &&
530
+ entry.signature.parameters.length === node.args.length &&
531
+ entry.signature.name.toLowerCase() === wanted &&
532
+ entry.signature.name !== node.name)
533
+ .map((entry) => entry.signature.name));
534
+ if (candidates.size !== 1)
535
+ return undefined;
536
+ return this.replacement(node, { ...node, name: [...candidates][0] });
537
+ }
538
+ /** The same call written in its other form, parenthesized as the tree requires. */
539
+ formFix(node) {
540
+ if (node.kind === "call") {
541
+ if (node.args.length === 0)
542
+ return undefined;
543
+ const [receiver, ...rest] = node.args;
544
+ return this.replacement(node, {
545
+ kind: "receiverCall",
546
+ receiver: receiver,
547
+ name: node.name,
548
+ nameRange: node.nameRange,
549
+ args: rest,
550
+ range: node.range,
551
+ });
552
+ }
553
+ return this.replacement(node, {
554
+ kind: "call",
555
+ name: node.name,
556
+ nameRange: node.nameRange,
557
+ args: [node.receiver, ...node.args],
558
+ range: node.range,
559
+ });
560
+ }
561
+ /** The whole expression with one node replaced, written back out. */
562
+ replacement(from, to) {
563
+ const rewritten = replaceNode(this.expression.root, from, to);
564
+ if (!rewritten)
565
+ return undefined;
566
+ try {
567
+ return { replacement: serializeTree(rewritten) };
568
+ }
569
+ catch {
570
+ // A tree with an unreadable part cannot be written back, so no fix is offered;
571
+ // the diagnostic itself stands.
572
+ return undefined;
573
+ }
574
+ }
575
+ qualifiedCallType(node) {
576
+ const args = node.args.map((argument) => this.typeOf(argument));
577
+ const qualified = `${node.namespace}.${node.name}`;
578
+ const declared = this.context.namespaceFunction(node.namespace, node.name);
579
+ if (!declared) {
580
+ this.calls.push({
581
+ name: qualified,
582
+ form: "receiver",
583
+ namespace: node.namespace,
584
+ arity: args.length,
585
+ range: node.range,
586
+ argumentTypes: args.map((type) => formatType(withoutParameters(type))),
587
+ });
588
+ // An OPEN namespace declares only part of what it reaches, so a name it does not
589
+ // carry is not this engine's to refuse — it is listed and left to the host.
590
+ if (this.context.namespaceIsOpen(node.namespace))
591
+ return DYN;
592
+ this.report("FUNCTION_UNRESOLVED", `${JSON.stringify(node.namespace)} declares no function named ${JSON.stringify(node.name)}`, node.range);
593
+ return DYN;
594
+ }
595
+ const { parameters } = declared;
596
+ this.calls.push({
597
+ name: qualified,
598
+ form: "receiver",
599
+ namespace: node.namespace,
600
+ arity: args.length,
601
+ range: node.range,
602
+ argumentTypes: args.map((type) => formatType(withoutParameters(type))),
603
+ ...(declared.signature === undefined ? {} : { signature: declared.signature }),
604
+ returns: formatType(withoutParameters(declared.returns)),
605
+ deterministic: declared.deterministic ?? true,
606
+ hostBacked: declared.hostBacked ?? false,
607
+ ...(declared.throws ? { throws: declared.throws } : {}),
608
+ });
609
+ // A type the host named and its own registry does not carry: the declaration stands,
610
+ // reads `dyn` where that type stood, and the disagreement is a verdict HERE — this is
611
+ // the only place the name has a range.
612
+ const unregistered = declared.unregisteredTypes ?? [];
613
+ if (unregistered.length > 0) {
614
+ const names = unregistered.map((name) => JSON.stringify(name)).join(", ");
615
+ this.report("CEL_TYPE_ERROR", unregistered.length === 1
616
+ ? `${qualified} is declared over the type ${names}, and no type is registered under that name`
617
+ : `${qualified} is declared over the types ${names}, and no type is registered under those names`, node.range);
618
+ }
619
+ // Parameters withheld: the host judges arity and arguments against its own, richer
620
+ // signature grammar, and this engine types the result and says nothing else.
621
+ if (parameters === undefined)
622
+ return declared.returns;
623
+ if (args.length !== parameters.length) {
624
+ this.report("FUNCTION_ARITY_MISMATCH", `${qualified} takes ${parameters.length} argument${parameters.length === 1 ? "" : "s"}, and ${args.length} ${args.length === 1 ? "is" : "are"} written`, node.range);
625
+ return declared.returns;
626
+ }
627
+ for (const [at, argument] of args.entries()) {
628
+ const wanted = parameters[at];
629
+ if (assignable(argument, wanted))
630
+ continue;
631
+ const mismatch = this.typeArgumentMismatch([argument, wanted]);
632
+ this.report(mismatch ? "CEL_TYPE_ARGUMENT_MISMATCH" : "FUNCTION_ARGUMENT_MISMATCH", mismatch ??
633
+ `${qualified} takes ${formatType(wanted)} here, and this argument is ${formatType(argument)}`, node.args[at].range);
634
+ }
635
+ return declared.returns;
636
+ }
637
+ }
638
+ /** Whether a comparison is a test against null of something that may be null. */
639
+ function isNullTest(args) {
640
+ if (args.length !== 2)
641
+ return false;
642
+ const [left, right] = args;
643
+ const isNull = (type) => type.kind === "primitive" && type.name === "null";
644
+ return (isNull(left) && admitsNull(right)) || (isNull(right) && admitsNull(left));
645
+ }
646
+ /**
647
+ * Which branches of a union a read is judged against.
648
+ *
649
+ * A **presence-shaped** read — `a.?b`, `a[?k]`, `has(a.b)` — asks whether the member is
650
+ * THERE, so a branch that can hold no member at all is the absence case it answers
651
+ * `optional.none()` / `false` for at runtime, not a mistake in the expression: that is how
652
+ * a two-shape field is discriminated without a type test. Where **no** branch can hold one
653
+ * the read is judged against them all, so an operand that is known to hold no members is
654
+ * still refused in the presence form exactly as in the plain one.
655
+ */
656
+ function presenceBranches(union, holds, presence) {
657
+ if (!presence)
658
+ return union.members;
659
+ const holding = union.members.filter(holds);
660
+ return holding.length > 0 ? holding : union.members;
661
+ }
662
+ /** Whether a type can hold a named member at all — what `memberType` reads one of. */
663
+ function holdsMembers(type) {
664
+ if (isDyn(type) || type.kind === "parameter")
665
+ return true;
666
+ if (type.kind === "record" || type.kind === "map")
667
+ return true;
668
+ if (type.kind === "optional")
669
+ return holdsMembers(type.value);
670
+ if (type.kind === "union")
671
+ return type.members.some(holdsMembers);
672
+ return false;
673
+ }
674
+ /** Whether a type can hold an element at all — what `elementType` reads one of. */
675
+ function holdsElements(type) {
676
+ if (isDyn(type) || type.kind === "parameter")
677
+ return true;
678
+ if (type.kind === "record" || type.kind === "map" || type.kind === "list")
679
+ return true;
680
+ if (type.kind === "optional")
681
+ return holdsElements(type.value);
682
+ if (type.kind === "union")
683
+ return type.members.some(holdsElements);
684
+ return false;
685
+ }
686
+ /** An optional of an optional is one optional; a chain of reads does not nest them. */
687
+ function unwrapOptional(type) {
688
+ return type.kind === "optional" ? unwrapOptional(type.value) : type;
689
+ }
690
+ /**
691
+ * The dotted path a select spells, when every step is a plain named member of a name.
692
+ * An optional step or an index makes it no longer one name.
693
+ */
694
+ function plainChain(node) {
695
+ const segments = [];
696
+ let at = node;
697
+ while (at.kind === "select") {
698
+ if (at.optional || at.field === "")
699
+ return undefined;
700
+ segments.unshift({ name: at.field, range: at.fieldRange });
701
+ at = at.operand;
702
+ }
703
+ if (at.kind !== "ident")
704
+ return undefined;
705
+ segments.unshift({ name: at.name, range: at.range });
706
+ return { root: at, segments };
707
+ }
708
+ function sameArguments(left, right) {
709
+ return left.length === right.length && left.every((type, at) => formatType(type) === formatType(right[at]));
710
+ }
711
+ function isOptionalMacro(node) {
712
+ if (node.kind !== "receiverCall")
713
+ return false;
714
+ if (node.name === "optMap" || node.name === "optFlatMap")
715
+ return true;
716
+ return node.receiver.kind === "ident" && node.receiver.name === "optional";
717
+ }
718
+ /** The tree with one node swapped, or nothing when the node is not in it. */
719
+ function replaceNode(root, from, to) {
720
+ if (root === from)
721
+ return to;
722
+ switch (root.kind) {
723
+ case "literal":
724
+ case "ident":
725
+ case "unparsed":
726
+ return undefined;
727
+ case "list": {
728
+ for (const [at, element] of root.elements.entries()) {
729
+ const value = replaceNode(element.value, from, to);
730
+ if (!value)
731
+ continue;
732
+ const elements = [...root.elements];
733
+ elements[at] = { ...element, value };
734
+ return { ...root, elements };
735
+ }
736
+ return undefined;
737
+ }
738
+ case "map": {
739
+ for (const [at, entry] of root.entries.entries()) {
740
+ const key = replaceNode(entry.key, from, to);
741
+ const value = replaceNode(entry.value, from, to);
742
+ if (!key && !value)
743
+ continue;
744
+ const entries = [...root.entries];
745
+ entries[at] = { ...entry, key: key ?? entry.key, value: value ?? entry.value };
746
+ return { ...root, entries };
747
+ }
748
+ return undefined;
749
+ }
750
+ case "select": {
751
+ const operand = replaceNode(root.operand, from, to);
752
+ return operand ? { ...root, operand } : undefined;
753
+ }
754
+ case "index": {
755
+ const operand = replaceNode(root.operand, from, to);
756
+ if (operand)
757
+ return { ...root, operand };
758
+ const index = replaceNode(root.index, from, to);
759
+ return index ? { ...root, index } : undefined;
760
+ }
761
+ case "call":
762
+ case "qcall": {
763
+ const args = replaceInList(root.args, from, to);
764
+ return args ? { ...root, args } : undefined;
765
+ }
766
+ case "receiverCall": {
767
+ const receiver = replaceNode(root.receiver, from, to);
768
+ if (receiver)
769
+ return { ...root, receiver };
770
+ const args = replaceInList(root.args, from, to);
771
+ return args ? { ...root, args } : undefined;
772
+ }
773
+ case "unary": {
774
+ const operand = replaceNode(root.operand, from, to);
775
+ return operand ? { ...root, operand } : undefined;
776
+ }
777
+ case "binary": {
778
+ const left = replaceNode(root.left, from, to);
779
+ if (left)
780
+ return { ...root, left };
781
+ const right = replaceNode(root.right, from, to);
782
+ return right ? { ...root, right } : undefined;
783
+ }
784
+ case "conditional": {
785
+ const condition = replaceNode(root.condition, from, to);
786
+ if (condition)
787
+ return { ...root, condition };
788
+ const whenTrue = replaceNode(root.whenTrue, from, to);
789
+ if (whenTrue)
790
+ return { ...root, whenTrue };
791
+ const whenFalse = replaceNode(root.whenFalse, from, to);
792
+ return whenFalse ? { ...root, whenFalse } : undefined;
793
+ }
794
+ }
795
+ }
796
+ function replaceInList(nodes, from, to) {
797
+ for (const [at, node] of nodes.entries()) {
798
+ const replaced = replaceNode(node, from, to);
799
+ if (!replaced)
800
+ continue;
801
+ const copy = [...nodes];
802
+ copy[at] = replaced;
803
+ return copy;
804
+ }
805
+ return undefined;
806
+ }