@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,721 @@
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
+ // --- the walk -----------------------------------------------------------
77
+ typeOf(node) {
78
+ switch (node.kind) {
79
+ case "unparsed":
80
+ return DYN;
81
+ case "literal":
82
+ return this.literalType(node.literal.type);
83
+ case "ident":
84
+ return this.identType(node);
85
+ case "list":
86
+ return this.listType(node);
87
+ case "map":
88
+ return this.mapType(node);
89
+ case "select":
90
+ return this.selectType(node);
91
+ case "index":
92
+ return this.indexType(node);
93
+ case "unary":
94
+ return this.operatorType(node.operator, [node.operand], node.range);
95
+ case "binary":
96
+ return this.binaryType(node);
97
+ case "conditional":
98
+ return this.conditionalType(node);
99
+ case "call":
100
+ case "receiverCall":
101
+ return this.callType(node);
102
+ case "qcall":
103
+ return this.qualifiedCallType(node);
104
+ }
105
+ }
106
+ literalType(type) {
107
+ switch (type) {
108
+ case "int":
109
+ return INT;
110
+ case "uint":
111
+ return UINT;
112
+ case "double":
113
+ return DOUBLE;
114
+ case "string":
115
+ return STRING;
116
+ case "bytes":
117
+ return BYTES;
118
+ case "bool":
119
+ return BOOL;
120
+ default:
121
+ return NULL;
122
+ }
123
+ }
124
+ identType(node) {
125
+ // An absolute name is resolved against the environment alone: that is what it is for.
126
+ if (!node.absolute) {
127
+ for (let at = this.scopes.length - 1; at >= 0; at -= 1) {
128
+ const bound = this.scopes[at].get(node.name);
129
+ if (bound)
130
+ return bound;
131
+ }
132
+ }
133
+ const declared = this.context.variable(node.name);
134
+ if (declared)
135
+ return declared;
136
+ if (this.context.options.unlistedVariablesAreDyn)
137
+ return DYN;
138
+ this.report("CEL_UNKNOWN_IDENTIFIER", `${JSON.stringify(node.name)} is not declared here${this.availableNames()}`, node.range);
139
+ return DYN;
140
+ }
141
+ availableNames() {
142
+ const names = this.context.declaredVariableNames();
143
+ return names.length === 0 ? "" : ` (declared: ${[...names].sort().join(", ")})`;
144
+ }
145
+ listType(node) {
146
+ if (node.elements.length === 0)
147
+ return listOf(parameterOf("T"));
148
+ const types = node.elements.map((element) => element.optional
149
+ ? this.heldByOptional(this.typeOf(element.value), element.value, "a list element")
150
+ : this.typeOf(element.value));
151
+ return listOf(this.reduce(types, node.elements.map((element) => element.value), "list"));
152
+ }
153
+ /**
154
+ * What an optional entry contributes: the value inside the optional. The entry holds an
155
+ * `optional<T>` and the aggregate holds `T`, because an absent one leaves no entry at
156
+ * all — which is the whole point of writing it that way.
157
+ */
158
+ heldByOptional(type, node, what) {
159
+ if (type.kind === "optional")
160
+ return type.value;
161
+ if (isDyn(type) || type.kind === "parameter")
162
+ return DYN;
163
+ this.report("CEL_TYPE_ERROR", `${what} written with '?' holds an optional, and this one is ${formatType(type)}`, node.range);
164
+ return DYN;
165
+ }
166
+ mapType(node) {
167
+ if (node.entries.length === 0)
168
+ return mapOf(parameterOf("K"), parameterOf("V"));
169
+ const keys = node.entries.map((entry) => this.typeOf(entry.key));
170
+ const values = node.entries.map((entry) => entry.optional
171
+ ? this.heldByOptional(this.typeOf(entry.value), entry.value, "a map entry")
172
+ : this.typeOf(entry.value));
173
+ return mapOf(this.reduce(keys, node.entries.map((entry) => entry.key), "map key"), this.reduce(values, node.entries.map((entry) => entry.value), "map value"));
174
+ }
175
+ /**
176
+ * One type for several written values. Mixed types are `dyn` — CEL's own reading of a
177
+ * heterogeneous literal — unless the environment asked for homogeneous aggregates, in
178
+ * which case the mixture is the mistake.
179
+ */
180
+ reduce(types, nodes, what) {
181
+ let reduced = types[0];
182
+ for (let at = 1; at < types.length; at += 1) {
183
+ const next = unify(reduced, types[at]);
184
+ if (this.context.options.homogeneousAggregateLiterals &&
185
+ isDyn(next) &&
186
+ !isDyn(reduced) &&
187
+ !isDyn(types[at])) {
188
+ this.report("CEL_TYPE_ERROR", `this ${what} is ${formatType(types[at])} where the others are ${formatType(reduced)}`, nodes[at].range);
189
+ }
190
+ reduced = next;
191
+ }
192
+ return reduced;
193
+ }
194
+ selectType(node) {
195
+ const qualified = this.qualifiedVariableType(node);
196
+ if (qualified)
197
+ return qualified;
198
+ const operand = this.typeOf(node.operand);
199
+ if (node.field === "")
200
+ return DYN;
201
+ const guarded = this.checkNullable(node.operand, operand, `.${node.field}`, node.fieldRange);
202
+ const held = this.memberType(guarded, node.field, node.fieldRange);
203
+ // Reading through an optional answers an optional, whichever form the read is
204
+ // written in: that is what makes a chain of reads over a value that may be absent
205
+ // stay one expression instead of needing a guard at every step.
206
+ return node.optional || guarded.kind === "optional" ? optionalOf(unwrapOptional(held)) : held;
207
+ }
208
+ /**
209
+ * A dotted declaration is **one name**, and the longest one wins.
210
+ *
211
+ * A host may declare `a.b.c`, or `a.b` holding a map, or both. `a.b.c` then reads the
212
+ * variable of that name where it is declared, and the map's entry where only `a.b` is —
213
+ * so the host's own naming decides, not the shape of the expression. A name something
214
+ * inside the expression bound takes precedence over a declaration, as a bare name does,
215
+ * unless the chain is absolute.
216
+ */
217
+ qualifiedVariableType(node) {
218
+ const chain = plainChain(node);
219
+ if (!chain)
220
+ return undefined;
221
+ if (!chain.root.absolute && this.isBound(chain.root.name))
222
+ return undefined;
223
+ // The one split, shared with the backend (`declared-chain.ts`): two readers of "which
224
+ // name is this chain" would let the checker type one name while evaluation reads another.
225
+ const segments = chain.segments.map((segment) => segment.name);
226
+ const split = splitDeclaredChain(segments, (name) => this.context.variable(name) !== undefined);
227
+ if (!split)
228
+ return undefined;
229
+ // A split at the ROOT alone is the ordinary read of a name and then its members, which the
230
+ // select walk beside this types already — including the nullable-access rule, which is a
231
+ // fact about the expression's shape rather than about which name it reads. Only a DOTTED
232
+ // declaration is this question; the backend uses the whole answer, so neither searches a
233
+ // prefix the other declared.
234
+ if (split.rest.length === segments.length - 1)
235
+ return undefined;
236
+ let held = this.context.variable(split.name);
237
+ for (const segment of chain.segments.slice(chain.segments.length - split.rest.length)) {
238
+ held = this.memberType(held, segment.name, segment.range);
239
+ }
240
+ return held;
241
+ }
242
+ isBound(name) {
243
+ return this.scopes.some((scope) => scope.has(name));
244
+ }
245
+ /** The type of a named member, reported against whatever the operand turned out to be. */
246
+ memberType(operand, field, range) {
247
+ if (isDyn(operand) || operand.kind === "parameter")
248
+ return DYN;
249
+ if (operand.kind === "optional")
250
+ return this.memberType(operand.value, field, range);
251
+ if (operand.kind === "union") {
252
+ return unionOf(operand.members.map((member) => this.memberType(member, field, range)));
253
+ }
254
+ if (operand.kind === "record")
255
+ return this.recordMemberType(operand, field, range);
256
+ if (operand.kind === "map") {
257
+ if (!assignable(STRING, operand.key)) {
258
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} is keyed by ${formatType(operand.key)}, so ${JSON.stringify(field)} cannot name an entry of it`, range);
259
+ return DYN;
260
+ }
261
+ return operand.value;
262
+ }
263
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} holds no members`, range);
264
+ return DYN;
265
+ }
266
+ recordMemberType(record, field, range) {
267
+ const held = record.fields.get(field);
268
+ if (held)
269
+ return held;
270
+ if (record.open)
271
+ return DYN;
272
+ const declared = [...record.fields.keys()];
273
+ this.report("CEL_UNKNOWN_FIELD", `${JSON.stringify(field)} is not declared here${declared.length === 0 ? "" : ` (declared: ${declared.join(", ")})`}`, range);
274
+ return DYN;
275
+ }
276
+ indexType(node) {
277
+ const operand = this.typeOf(node.operand);
278
+ const index = this.typeOf(node.index);
279
+ const guarded = this.checkNullable(node.operand, operand, "[…]", node.range);
280
+ const held = this.elementType(guarded, index, node);
281
+ return node.optional || guarded.kind === "optional" ? optionalOf(unwrapOptional(held)) : held;
282
+ }
283
+ elementType(operand, index, node) {
284
+ if (isDyn(operand) || operand.kind === "parameter")
285
+ return DYN;
286
+ if (operand.kind === "optional")
287
+ return this.elementType(operand.value, index, node);
288
+ if (operand.kind === "union") {
289
+ return unionOf(operand.members.map((member) => this.elementType(member, index, node)));
290
+ }
291
+ if (operand.kind === "list") {
292
+ if (!assignable(index, INT) && !isDyn(index)) {
293
+ this.report("CEL_TYPE_ERROR", `a list is indexed by int, and this index is ${formatType(index)}`, node.index.range);
294
+ }
295
+ return operand.element;
296
+ }
297
+ if (operand.kind === "map") {
298
+ if (!assignable(index, operand.key) && !isDyn(index)) {
299
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} is indexed by ${formatType(operand.key)}, and this index is ${formatType(index)}`, node.index.range);
300
+ }
301
+ return operand.value;
302
+ }
303
+ if (operand.kind === "record") {
304
+ const literal = node.index.kind === "literal" && node.index.literal.type === "string";
305
+ if (literal) {
306
+ return this.recordMemberType(operand, node.index.literal.value, node.index.range);
307
+ }
308
+ return DYN;
309
+ }
310
+ this.report("CEL_TYPE_ERROR", `${formatType(operand)} holds no elements`, node.range);
311
+ return DYN;
312
+ }
313
+ binaryType(node) {
314
+ if (node.operator === "&&" || node.operator === "||") {
315
+ const left = this.typeOf(node.left);
316
+ const proofs = node.operator === "&&" ? provenWhereTrue(node.left) : provenWhereFalse(node.left);
317
+ const right = this.withProven(proofs, () => this.typeOf(node.right));
318
+ for (const type of [left, right]) {
319
+ if (!isDyn(type) && !assignable(type, BOOL)) {
320
+ this.report("CEL_TYPE_ERROR", `${node.operator} joins bool values, and ${formatType(type)} is not one`, node.range);
321
+ }
322
+ }
323
+ return BOOL;
324
+ }
325
+ return this.operatorType(node.operator, [node.left, node.right], node.range);
326
+ }
327
+ conditionalType(node) {
328
+ const condition = this.typeOf(node.condition);
329
+ if (!isDyn(condition) && !assignable(condition, BOOL)) {
330
+ this.report("CEL_TYPE_ERROR", `a condition must be bool, and this one is ${formatType(condition)}`, node.condition.range);
331
+ }
332
+ const whenTrue = this.withProven(provenWhereTrue(node.condition), () => this.typeOf(node.whenTrue));
333
+ const whenFalse = this.withProven(provenWhereFalse(node.condition), () => this.typeOf(node.whenFalse));
334
+ const unified = unify(whenTrue, whenFalse);
335
+ // A value read out of a conditional has one type, so two branches that share none
336
+ // is a mistake rather than a `dyn`: whoever reads it would be reading two things.
337
+ if (isDyn(unified) && !isDyn(whenTrue) && !isDyn(whenFalse)) {
338
+ this.report("CEL_TYPE_ERROR", `the branches of a condition answer ${formatType(whenTrue)} and ${formatType(whenFalse)}, which are not one type`, node.range);
339
+ }
340
+ return unified;
341
+ }
342
+ withProven(proofs, body) {
343
+ if (proofs.length === 0)
344
+ return body();
345
+ const outer = this.proven;
346
+ this.proven = new Set([...outer, ...proofs]);
347
+ try {
348
+ return body();
349
+ }
350
+ finally {
351
+ this.proven = outer;
352
+ }
353
+ }
354
+ /**
355
+ * A dereference of something that may be null. The type is answered with null taken
356
+ * out either way, so one missing guard is one diagnostic rather than a cascade.
357
+ */
358
+ checkNullable(operand, type, access, range) {
359
+ if (!admitsNull(type))
360
+ return type;
361
+ const chain = chainText(operand);
362
+ if (chain === undefined || this.proven.has(chain))
363
+ return withoutNull(type);
364
+ // **A chain rooted at a name the expression BOUND is not a subject of this verdict.** The
365
+ // guard constructs are only half the rule: the walk this replaces skipped such a chain
366
+ // entirely, so `xs.map(e, e.code)` over a nullable element is accepted today, and
367
+ // reporting it after the swap would newly reject a manifest — which is exactly what the
368
+ // guard set is held to its three forms to prevent.
369
+ const root = chainRoot(operand);
370
+ if (root !== undefined && this.isBound(root))
371
+ return withoutNull(type);
372
+ if (type.kind === "primitive") {
373
+ this.report("CEL_TYPE_ERROR", `null holds no members`, range);
374
+ return DYN;
375
+ }
376
+ 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);
377
+ return withoutNull(type);
378
+ }
379
+ operatorType(operator, operands, range) {
380
+ const args = operands.map((operand) => this.typeOf(operand));
381
+ // A value whose declared type admits null may always be tested against null —
382
+ // otherwise declaring it nullable would make it untestable, and the guard that
383
+ // clears a nullable read would itself be a type error.
384
+ if ((operator === "==" || operator === "!=") && isNullTest(args))
385
+ return BOOL;
386
+ const resolution = this.context.registry.resolve(operator, "global", args);
387
+ if ("resolved" in resolution)
388
+ return resolution.returns;
389
+ const mismatch = this.typeArgumentMismatch([...args, ...this.candidateTypes(resolution)]);
390
+ this.report(mismatch ? "CEL_TYPE_ARGUMENT_MISMATCH" : "CEL_TYPE_ERROR", mismatch ?? `no ${JSON.stringify(operator)} is declared over ${describeArguments(args)}`, range);
391
+ return DYN;
392
+ }
393
+ /**
394
+ * Two named types of one name whose arguments differ: invariance, so a mistake of its
395
+ * own rather than a plain type error. Only fully resolved types are compared — a
396
+ * signature's own parameter says nothing about what the call wanted.
397
+ */
398
+ typeArgumentMismatch(types) {
399
+ const nominals = types.filter((type) => type.kind === "nominal" && type.args.every((argument) => argument.kind !== "parameter"));
400
+ for (const [at, left] of nominals.entries()) {
401
+ for (const right of nominals.slice(at + 1)) {
402
+ if (left.name === right.name && !sameArguments(left.args, right.args)) {
403
+ return `${formatType(left)} and ${formatType(right)} are the same type with different type arguments, which never match`;
404
+ }
405
+ }
406
+ }
407
+ return undefined;
408
+ }
409
+ /** Every type a set of candidates mentions, for naming a type-argument mismatch. */
410
+ candidateTypes(failure) {
411
+ return failure.candidates.flatMap((candidate) => [
412
+ ...(candidate.signature.receiver ? [candidate.signature.receiver] : []),
413
+ ...candidate.signature.parameters,
414
+ ]);
415
+ }
416
+ callType(node) {
417
+ if (isMacroCall(node)) {
418
+ if (!this.context.options.enableOptionalTypes && isOptionalMacro(node)) {
419
+ this.report("CEL_UNKNOWN_FUNCTION", "optional types are not enabled in this environment", node.range);
420
+ return DYN;
421
+ }
422
+ return checkMacro(node, this);
423
+ }
424
+ const form = node.kind === "call" ? "global" : "receiver";
425
+ const receiver = node.kind === "receiverCall" ? this.typeOf(node.receiver) : undefined;
426
+ const args = node.args.map((argument) => this.typeOf(argument));
427
+ const resolution = this.context.registry.resolve(node.name, form, args, receiver);
428
+ if ("resolved" in resolution) {
429
+ this.calls.push(this.dispatched(node, form, args.length, resolution.resolved, resolution.returns));
430
+ this.checkLiteralArguments(node, resolution.resolved);
431
+ return resolution.returns;
432
+ }
433
+ this.calls.push({ name: node.name, form, arity: args.length, range: node.range });
434
+ this.reportCallFailure(node, form, args, receiver, resolution);
435
+ return DYN;
436
+ }
437
+ /**
438
+ * A registration's own guard over the arguments written as literals.
439
+ *
440
+ * It is asked here, where the call has just resolved, and nowhere else: a refusal over a
441
+ * VALUE is a verdict about the expression, so the component that decides every other
442
+ * verdict decides this one too — an implementation that refused the same argument at
443
+ * evaluation would leave a defect the source states behind a run. The refusal names the
444
+ * call as it was written, which is the one thing a reader needs to find it.
445
+ */
446
+ checkLiteralArguments(node, resolved) {
447
+ const check = resolved.metadata.checkArguments;
448
+ if (check === undefined)
449
+ return;
450
+ const written = node.kind === "receiverCall" ? [node.receiver, ...node.args] : node.args;
451
+ const refusal = check(written.map((argument) => (argument.kind === "literal" ? literalValue(argument.literal) : undefined)));
452
+ if (refusal === undefined)
453
+ return;
454
+ const source = this.expression.source.slice(node.range[0], node.range[1]);
455
+ this.report("CEL_INVALID_ARGUMENT", `${refusal} (in \`${source}\`)`, node.range);
456
+ }
457
+ dispatched(node, form, arity, resolved, returns) {
458
+ const { metadata } = resolved;
459
+ return {
460
+ name: node.name,
461
+ form,
462
+ arity,
463
+ range: node.range,
464
+ signature: formatSignature(resolved.signature),
465
+ returns: formatType(withoutParameters(returns)),
466
+ deterministic: metadata.deterministic ?? true,
467
+ hostBacked: metadata.hostBacked ?? false,
468
+ ...(metadata.throws ? { throws: metadata.throws } : {}),
469
+ };
470
+ }
471
+ reportCallFailure(node, form, args, receiver, failure) {
472
+ const written = `${node.name}(${describeArguments(args)})`;
473
+ if (failure.reason === "unknown") {
474
+ this.report("CEL_UNKNOWN_FUNCTION", `no function named ${JSON.stringify(node.name)} is registered`, node.range, this.renameFix(node));
475
+ return;
476
+ }
477
+ if (failure.reason === "wrong-form") {
478
+ const wanted = form === "global" ? "on a value" : "without a receiver";
479
+ this.report("CEL_WRONG_CALL_FORM", `${JSON.stringify(node.name)} is called ${wanted}: ${describeCandidates(failure.candidates)}`, node.range, this.formFix(node));
480
+ return;
481
+ }
482
+ const mismatch = this.typeArgumentMismatch([
483
+ ...(receiver ? [receiver] : []),
484
+ ...args,
485
+ ...this.candidateTypes(failure),
486
+ ]);
487
+ this.report(mismatch ? "CEL_TYPE_ARGUMENT_MISMATCH" : "CEL_TYPE_ERROR", mismatch ??
488
+ `no overload of ${JSON.stringify(node.name)} takes ${form === "receiver" ? `${formatType(receiver ?? DYN)}.` : ""}${written}: ${describeCandidates(failure.candidates)}`, node.range);
489
+ }
490
+ /** The one registered name this call might have meant, as a whole-source repair. */
491
+ renameFix(node) {
492
+ const wanted = node.name.toLowerCase();
493
+ const form = node.kind === "call" ? "global" : "receiver";
494
+ const candidates = new Set(this.context.registry
495
+ .list()
496
+ .filter((entry) => entry.signature.form === form &&
497
+ entry.signature.parameters.length === node.args.length &&
498
+ entry.signature.name.toLowerCase() === wanted &&
499
+ entry.signature.name !== node.name)
500
+ .map((entry) => entry.signature.name));
501
+ if (candidates.size !== 1)
502
+ return undefined;
503
+ return this.replacement(node, { ...node, name: [...candidates][0] });
504
+ }
505
+ /** The same call written in its other form, parenthesized as the tree requires. */
506
+ formFix(node) {
507
+ if (node.kind === "call") {
508
+ if (node.args.length === 0)
509
+ return undefined;
510
+ const [receiver, ...rest] = node.args;
511
+ return this.replacement(node, {
512
+ kind: "receiverCall",
513
+ receiver: receiver,
514
+ name: node.name,
515
+ nameRange: node.nameRange,
516
+ args: rest,
517
+ range: node.range,
518
+ });
519
+ }
520
+ return this.replacement(node, {
521
+ kind: "call",
522
+ name: node.name,
523
+ nameRange: node.nameRange,
524
+ args: [node.receiver, ...node.args],
525
+ range: node.range,
526
+ });
527
+ }
528
+ /** The whole expression with one node replaced, written back out. */
529
+ replacement(from, to) {
530
+ const rewritten = replaceNode(this.expression.root, from, to);
531
+ if (!rewritten)
532
+ return undefined;
533
+ try {
534
+ return { replacement: serializeTree(rewritten) };
535
+ }
536
+ catch {
537
+ // A tree with an unreadable part cannot be written back, so no fix is offered;
538
+ // the diagnostic itself stands.
539
+ return undefined;
540
+ }
541
+ }
542
+ qualifiedCallType(node) {
543
+ const args = node.args.map((argument) => this.typeOf(argument));
544
+ const qualified = `${node.namespace}.${node.name}`;
545
+ const declared = this.context.namespaceFunction(node.namespace, node.name);
546
+ if (!declared) {
547
+ this.calls.push({
548
+ name: qualified,
549
+ form: "receiver",
550
+ namespace: node.namespace,
551
+ arity: args.length,
552
+ range: node.range,
553
+ });
554
+ // An OPEN namespace declares only part of what it reaches, so a name it does not
555
+ // carry is not this engine's to refuse — it is listed and left to the host.
556
+ if (this.context.namespaceIsOpen(node.namespace))
557
+ return DYN;
558
+ this.report("FUNCTION_UNRESOLVED", `${JSON.stringify(node.namespace)} declares no function named ${JSON.stringify(node.name)}`, node.range);
559
+ return DYN;
560
+ }
561
+ const { parameters } = declared;
562
+ this.calls.push({
563
+ name: qualified,
564
+ form: "receiver",
565
+ namespace: node.namespace,
566
+ arity: args.length,
567
+ range: node.range,
568
+ ...(declared.signature === undefined ? {} : { signature: declared.signature }),
569
+ returns: formatType(withoutParameters(declared.returns)),
570
+ deterministic: declared.deterministic ?? true,
571
+ hostBacked: declared.hostBacked ?? false,
572
+ ...(declared.throws ? { throws: declared.throws } : {}),
573
+ });
574
+ // Parameters withheld: the host judges arity and arguments against its own, richer
575
+ // signature grammar, and this engine types the result and says nothing else.
576
+ if (parameters === undefined)
577
+ return declared.returns;
578
+ if (args.length !== parameters.length) {
579
+ 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);
580
+ return declared.returns;
581
+ }
582
+ for (const [at, argument] of args.entries()) {
583
+ const wanted = parameters[at];
584
+ if (assignable(argument, wanted))
585
+ continue;
586
+ const mismatch = this.typeArgumentMismatch([argument, wanted]);
587
+ this.report(mismatch ? "CEL_TYPE_ARGUMENT_MISMATCH" : "FUNCTION_ARGUMENT_MISMATCH", mismatch ??
588
+ `${qualified} takes ${formatType(wanted)} here, and this argument is ${formatType(argument)}`, node.args[at].range);
589
+ }
590
+ return declared.returns;
591
+ }
592
+ }
593
+ /** Whether a comparison is a test against null of something that may be null. */
594
+ function isNullTest(args) {
595
+ if (args.length !== 2)
596
+ return false;
597
+ const [left, right] = args;
598
+ const isNull = (type) => type.kind === "primitive" && type.name === "null";
599
+ return (isNull(left) && admitsNull(right)) || (isNull(right) && admitsNull(left));
600
+ }
601
+ /** An optional of an optional is one optional; a chain of reads does not nest them. */
602
+ function unwrapOptional(type) {
603
+ return type.kind === "optional" ? unwrapOptional(type.value) : type;
604
+ }
605
+ /**
606
+ * The dotted path a select spells, when every step is a plain named member of a name.
607
+ * An optional step or an index makes it no longer one name.
608
+ */
609
+ function plainChain(node) {
610
+ const segments = [];
611
+ let at = node;
612
+ while (at.kind === "select") {
613
+ if (at.optional || at.field === "")
614
+ return undefined;
615
+ segments.unshift({ name: at.field, range: at.fieldRange });
616
+ at = at.operand;
617
+ }
618
+ if (at.kind !== "ident")
619
+ return undefined;
620
+ segments.unshift({ name: at.name, range: at.range });
621
+ return { root: at, segments };
622
+ }
623
+ function sameArguments(left, right) {
624
+ return left.length === right.length && left.every((type, at) => formatType(type) === formatType(right[at]));
625
+ }
626
+ function isOptionalMacro(node) {
627
+ if (node.kind !== "receiverCall")
628
+ return false;
629
+ if (node.name === "optMap" || node.name === "optFlatMap")
630
+ return true;
631
+ return node.receiver.kind === "ident" && node.receiver.name === "optional";
632
+ }
633
+ /** The tree with one node swapped, or nothing when the node is not in it. */
634
+ function replaceNode(root, from, to) {
635
+ if (root === from)
636
+ return to;
637
+ switch (root.kind) {
638
+ case "literal":
639
+ case "ident":
640
+ case "unparsed":
641
+ return undefined;
642
+ case "list": {
643
+ for (const [at, element] of root.elements.entries()) {
644
+ const value = replaceNode(element.value, from, to);
645
+ if (!value)
646
+ continue;
647
+ const elements = [...root.elements];
648
+ elements[at] = { ...element, value };
649
+ return { ...root, elements };
650
+ }
651
+ return undefined;
652
+ }
653
+ case "map": {
654
+ for (const [at, entry] of root.entries.entries()) {
655
+ const key = replaceNode(entry.key, from, to);
656
+ const value = replaceNode(entry.value, from, to);
657
+ if (!key && !value)
658
+ continue;
659
+ const entries = [...root.entries];
660
+ entries[at] = { ...entry, key: key ?? entry.key, value: value ?? entry.value };
661
+ return { ...root, entries };
662
+ }
663
+ return undefined;
664
+ }
665
+ case "select": {
666
+ const operand = replaceNode(root.operand, from, to);
667
+ return operand ? { ...root, operand } : undefined;
668
+ }
669
+ case "index": {
670
+ const operand = replaceNode(root.operand, from, to);
671
+ if (operand)
672
+ return { ...root, operand };
673
+ const index = replaceNode(root.index, from, to);
674
+ return index ? { ...root, index } : undefined;
675
+ }
676
+ case "call":
677
+ case "qcall": {
678
+ const args = replaceInList(root.args, from, to);
679
+ return args ? { ...root, args } : undefined;
680
+ }
681
+ case "receiverCall": {
682
+ const receiver = replaceNode(root.receiver, from, to);
683
+ if (receiver)
684
+ return { ...root, receiver };
685
+ const args = replaceInList(root.args, from, to);
686
+ return args ? { ...root, args } : undefined;
687
+ }
688
+ case "unary": {
689
+ const operand = replaceNode(root.operand, from, to);
690
+ return operand ? { ...root, operand } : undefined;
691
+ }
692
+ case "binary": {
693
+ const left = replaceNode(root.left, from, to);
694
+ if (left)
695
+ return { ...root, left };
696
+ const right = replaceNode(root.right, from, to);
697
+ return right ? { ...root, right } : undefined;
698
+ }
699
+ case "conditional": {
700
+ const condition = replaceNode(root.condition, from, to);
701
+ if (condition)
702
+ return { ...root, condition };
703
+ const whenTrue = replaceNode(root.whenTrue, from, to);
704
+ if (whenTrue)
705
+ return { ...root, whenTrue };
706
+ const whenFalse = replaceNode(root.whenFalse, from, to);
707
+ return whenFalse ? { ...root, whenFalse } : undefined;
708
+ }
709
+ }
710
+ }
711
+ function replaceInList(nodes, from, to) {
712
+ for (const [at, node] of nodes.entries()) {
713
+ const replaced = replaceNode(node, from, to);
714
+ if (!replaced)
715
+ continue;
716
+ const copy = [...nodes];
717
+ copy[at] = replaced;
718
+ return copy;
719
+ }
720
+ return undefined;
721
+ }