@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
package/src/parser.ts ADDED
@@ -0,0 +1,554 @@
1
+ /**
2
+ * CEL's grammar, read by recursive descent into the canonical tree.
3
+ *
4
+ * **Error recovery is the point.** Parsing never throws and never discards what it
5
+ * read: the first thing it cannot read becomes one ranged diagnostic, the reader
6
+ * stops there, and the tree holds the longest prefix it understood with an
7
+ * `unparsed` node where the rest would have been. That is what lets completion and
8
+ * hover work on an expression mid-token, and nothing downstream has to re-parse a
9
+ * shortened prefix to get an answer. A member left unnamed (`request.`) is a select
10
+ * with an **empty field name**, which is the shape completion after a dot reads; an
11
+ * aggregate or a call left open keeps the elements it read. Neither can be
12
+ * serialized, and the diagnostic is what says the tree is incomplete.
13
+ *
14
+ * **Nothing is expanded.** A macro call (`has(x)`, `xs.map(i, i)`, `cel.bind(…)`) is
15
+ * an ordinary call node: expanding it here would make the serializer unable to
16
+ * write the source back, and lowering it to a comprehension is the checker's and a
17
+ * backend's business. A qualified call is not produced here either — see
18
+ * `namespace-resolution.ts`.
19
+ *
20
+ * **A unary minus directly on a numeric literal folds into the literal.** That is
21
+ * how the int64 minimum is written at all, and it keeps `-0.0` a value rather than
22
+ * a negation of zero. A minus on a parenthesized literal does not fold, so the
23
+ * serializer can write a hand-built negation back without changing its shape.
24
+ */
25
+
26
+ import { type Token, tokenize } from "./lexer.js";
27
+ import { type CelParseLimits, resolveParseLimits } from "./parse-limits.js";
28
+ import type { CelSyntaxDiagnostic } from "./syntax-diagnostic.js";
29
+ import { FirstSyntaxDiagnostic } from "./syntax-diagnostic.js";
30
+ import type {
31
+ CelBinaryOperator,
32
+ CelListElement,
33
+ CelMapEntry,
34
+ CelNode,
35
+ CelUnaryOperator,
36
+ SourceRange,
37
+ } from "./syntax-tree.js";
38
+
39
+ export interface ParseOptions {
40
+ readonly limits?: Partial<CelParseLimits>;
41
+ /**
42
+ * Whether the optional library's own syntax is read: `[?x]` and `{?k: v}`, whose entry
43
+ * holds an optional and contributes its value type. It follows the environment's
44
+ * `enableOptionalTypes`, because a syntax for a type that does not exist would parse to
45
+ * a tree nothing can check.
46
+ */
47
+ readonly optionalSyntax?: boolean;
48
+ }
49
+
50
+ export interface ParseResult {
51
+ readonly source: string;
52
+ readonly root: CelNode;
53
+ readonly diagnostics: readonly CelSyntaxDiagnostic[];
54
+ }
55
+
56
+ const RELATIONS: readonly string[] = ["==", "!=", "<", "<=", ">", ">="];
57
+ const ADDITIVE: readonly string[] = ["+", "-"];
58
+ const MULTIPLICATIVE: readonly string[] = ["*", "/", "%"];
59
+
60
+ class Parser {
61
+ private at = 0;
62
+ private nodes = 0;
63
+ private depth = 0;
64
+ private stopped = false;
65
+
66
+ constructor(
67
+ private readonly source: string,
68
+ private readonly tokens: readonly Token[],
69
+ private readonly diagnostics: FirstSyntaxDiagnostic,
70
+ private readonly limits: CelParseLimits,
71
+ private readonly optionalSyntax: boolean,
72
+ ) {
73
+ if (diagnostics.reported) this.stopped = true;
74
+ }
75
+
76
+ parse(): CelNode {
77
+ const root = this.conditional();
78
+ if (!this.stopped && this.peek().type !== "eof") this.unexpected(this.peek());
79
+ return root;
80
+ }
81
+
82
+ // --- token access -------------------------------------------------------
83
+
84
+ private peek(ahead = 0): Token {
85
+ return this.tokens[Math.min(this.at + ahead, this.tokens.length - 1)]!;
86
+ }
87
+
88
+ private advance(): Token {
89
+ const token = this.peek();
90
+ if (this.at < this.tokens.length - 1) this.at += 1;
91
+ return token;
92
+ }
93
+
94
+ private isPunct(text: string): boolean {
95
+ const token = this.peek();
96
+ return token.type === "punct" && token.text === text;
97
+ }
98
+
99
+ private takePunct(text: string): boolean {
100
+ if (!this.isPunct(text)) return false;
101
+ this.advance();
102
+ return true;
103
+ }
104
+
105
+ private expectPunct(text: string): boolean {
106
+ if (this.takePunct(text)) return true;
107
+ this.unexpected(this.peek(), text);
108
+ return false;
109
+ }
110
+
111
+ // --- diagnostics and budgets -------------------------------------------
112
+
113
+ private unexpected(token: Token, expected?: string): void {
114
+ const wanted = expected === undefined ? "" : `, expected ${JSON.stringify(expected)}`;
115
+ if (token.type === "eof") {
116
+ this.diagnostics.report(
117
+ "unexpected_end",
118
+ `the expression ends before it is complete${wanted}`,
119
+ token.start,
120
+ token.end,
121
+ );
122
+ } else {
123
+ this.diagnostics.report(
124
+ "unexpected_token",
125
+ `${JSON.stringify(this.source.slice(token.start, token.end))} cannot stand here${wanted}`,
126
+ token.start,
127
+ token.end,
128
+ );
129
+ }
130
+ this.stopped = true;
131
+ }
132
+
133
+ private limit(what: string, max: number, start: number, end: number): void {
134
+ this.diagnostics.report(
135
+ "limit_exceeded",
136
+ `the expression has more ${what} than the limit of ${max}`,
137
+ start,
138
+ end,
139
+ );
140
+ this.stopped = true;
141
+ }
142
+
143
+ /** Counts a node against the node budget. */
144
+ private keep<T extends CelNode>(node: T): T {
145
+ this.nodes += 1;
146
+ if (this.nodes > this.limits.maxNodes && !this.stopped) {
147
+ this.limit("nodes", this.limits.maxNodes, node.range[0], node.range[1]);
148
+ }
149
+ return node;
150
+ }
151
+
152
+ private enter(start: number): boolean {
153
+ this.depth += 1;
154
+ if (this.depth <= this.limits.maxDepth) return true;
155
+ if (!this.stopped) this.limit("nesting", this.limits.maxDepth, start, start);
156
+ return false;
157
+ }
158
+
159
+ private leave(): void {
160
+ this.depth -= 1;
161
+ }
162
+
163
+ private unparsed(start: number, end = start): CelNode {
164
+ return this.keep({ kind: "unparsed", range: [start, end] as SourceRange });
165
+ }
166
+
167
+ private spanning(left: CelNode, right: CelNode): SourceRange {
168
+ return [left.range[0], right.range[1]];
169
+ }
170
+
171
+ // --- the grammar -------------------------------------------------------
172
+
173
+ private conditional(): CelNode {
174
+ const start = this.peek().start;
175
+ if (!this.enter(start)) return this.unparsed(start);
176
+ try {
177
+ const condition = this.binary(0);
178
+ if (this.stopped || !this.isPunct("?")) return condition;
179
+ this.advance();
180
+ const whenTrue = this.conditional();
181
+ const whenFalse =
182
+ this.stopped || !this.expectPunct(":")
183
+ ? this.unparsed(whenTrue.range[1])
184
+ : this.conditional();
185
+ return this.keep({
186
+ kind: "conditional",
187
+ condition,
188
+ whenTrue,
189
+ whenFalse,
190
+ range: this.spanning(condition, whenFalse),
191
+ });
192
+ } finally {
193
+ this.leave();
194
+ }
195
+ }
196
+
197
+ /**
198
+ * The binary levels, lowest first: `||`, `&&`, the relations, `+ -`, `* / %`.
199
+ * One loop per level, each left-associative, so a long chain costs no depth.
200
+ */
201
+ private binary(level: number): CelNode {
202
+ if (level >= 5) return this.unary();
203
+ let left = this.binary(level + 1);
204
+ while (!this.stopped) {
205
+ const operator = this.binaryOperatorAt(level);
206
+ if (!operator) return left;
207
+ this.advance();
208
+ const right = this.binary(level + 1);
209
+ left = this.keep({ kind: "binary", operator, left, right, range: this.spanning(left, right) });
210
+ if (right.kind === "unparsed") return left;
211
+ }
212
+ return left;
213
+ }
214
+
215
+ private binaryOperatorAt(level: number): CelBinaryOperator | undefined {
216
+ const token = this.peek();
217
+ if (level === 2 && token.type === "keyword" && token.text === "in") return "in";
218
+ if (token.type !== "punct") return undefined;
219
+ const text = token.text;
220
+ const wanted =
221
+ level === 0 ? ["||"] : level === 1 ? ["&&"] : level === 2 ? RELATIONS : level === 3 ? ADDITIVE : MULTIPLICATIVE;
222
+ return wanted.includes(text) ? (text as CelBinaryOperator) : undefined;
223
+ }
224
+
225
+ private unary(): CelNode {
226
+ const token = this.peek();
227
+ if (token.type === "punct" && (token.text === "!" || token.text === "-")) {
228
+ const folded = token.text === "-" ? this.foldedNumber(token) : undefined;
229
+ if (folded) return this.postfix(folded);
230
+ if (!this.enter(token.start)) return this.unparsed(token.start);
231
+ try {
232
+ this.advance();
233
+ const operand = this.unary();
234
+ return this.keep({
235
+ kind: "unary",
236
+ operator: token.text as CelUnaryOperator,
237
+ operand,
238
+ range: [token.start, operand.range[1]],
239
+ });
240
+ } finally {
241
+ this.leave();
242
+ }
243
+ }
244
+ return this.postfix(this.primary());
245
+ }
246
+
247
+ /** `-` directly on an int or double literal is part of the literal. */
248
+ private foldedNumber(minus: Token): CelNode | undefined {
249
+ const number = this.peek(1);
250
+ if (number.type !== "int" && number.type !== "double") return undefined;
251
+ this.advance();
252
+ this.advance();
253
+ const literal =
254
+ number.type === "int"
255
+ ? ({ type: "int", value: -number.int! } as const)
256
+ : ({ type: "double", value: -number.double! } as const);
257
+ return this.keep({ kind: "literal", literal, range: [minus.start, number.end] });
258
+ }
259
+
260
+ private postfix(operand: CelNode): CelNode {
261
+ let node = operand;
262
+ while (!this.stopped) {
263
+ if (this.takePunct(".")) {
264
+ const optional = this.takePunct("?");
265
+ const name = this.peek();
266
+ const quoted = name.type === "quotedIdent";
267
+ if (!this.expectMemberName(name)) {
268
+ // The member is unnamed: the select stands with an empty field, which is
269
+ // what an editor reads to complete a member after the dot.
270
+ return this.keep({
271
+ kind: "select",
272
+ operand: node,
273
+ field: "",
274
+ fieldRange: [name.start, name.start],
275
+ optional,
276
+ quoted: false,
277
+ range: [node.range[0], name.start],
278
+ });
279
+ }
280
+ this.advance();
281
+ const fieldRange: SourceRange = [name.start, name.end];
282
+ if (quoted && this.isPunct("(")) {
283
+ // cel-spec admits a quoted name where a FIELD is read (`member '.' escapeIdent`)
284
+ // and nowhere else: a called function is always an identifier. Keeping that line
285
+ // is also what keeps a namespaced call to exactly one spelling.
286
+ this.unexpected(this.peek(), "a member read — a quoted name is a field, not a call");
287
+ return this.keep({
288
+ kind: "select",
289
+ operand: node,
290
+ field: name.text,
291
+ fieldRange,
292
+ optional,
293
+ quoted,
294
+ range: [node.range[0], name.end],
295
+ });
296
+ }
297
+ if (this.isPunct("(")) {
298
+ const args = this.callArguments();
299
+ node = this.keep({
300
+ kind: "receiverCall",
301
+ receiver: node,
302
+ name: name.text,
303
+ nameRange: fieldRange,
304
+ args: args.nodes,
305
+ range: [node.range[0], args.end],
306
+ });
307
+ continue;
308
+ }
309
+ node = this.keep({
310
+ kind: "select",
311
+ operand: node,
312
+ field: name.text,
313
+ fieldRange,
314
+ optional,
315
+ quoted,
316
+ range: [node.range[0], name.end],
317
+ });
318
+ continue;
319
+ }
320
+ if (this.isPunct("[")) {
321
+ this.advance();
322
+ const optional = this.takePunct("?");
323
+ const index = this.conditional();
324
+ const closed = !this.stopped && this.expectPunct("]");
325
+ const end = closed ? this.tokens[this.at - 1]!.end : index.range[1];
326
+ node = this.keep({ kind: "index", operand: node, index, optional, range: [node.range[0], end] });
327
+ if (!closed) return node;
328
+ continue;
329
+ }
330
+ return node;
331
+ }
332
+ return node;
333
+ }
334
+
335
+ /**
336
+ * A field or a called function may be named by any word, however that word is read
337
+ * elsewhere — `{'let': 1}.let`, `a.in` and `a.true` are all CEL. A member names a
338
+ * value's entry, not a name in the expression's scope.
339
+ */
340
+ private expectMemberName(token: Token): boolean {
341
+ if (token.type === "ident" || token.type === "reserved" || token.type === "keyword") return true;
342
+ // A name between backticks, which is how a member not spelled as an identifier is read.
343
+ if (token.type === "quotedIdent") return true;
344
+ return this.expectIdentifier(token);
345
+ }
346
+
347
+ private expectIdentifier(token: Token): boolean {
348
+ if (token.type === "ident") return true;
349
+ if (token.type === "reserved") {
350
+ this.diagnostics.report(
351
+ "reserved_identifier",
352
+ `${JSON.stringify(token.text)} is a reserved word and cannot be used as a name`,
353
+ token.start,
354
+ token.end,
355
+ );
356
+ this.stopped = true;
357
+ return false;
358
+ }
359
+ this.unexpected(token, "a name");
360
+ return false;
361
+ }
362
+
363
+ private primary(): CelNode {
364
+ const token = this.peek();
365
+ switch (token.type) {
366
+ case "int":
367
+ this.advance();
368
+ if (token.atIntBoundary) {
369
+ // 2^63 is a magnitude, not a value: it is only the int64 minimum, which
370
+ // the fold under a unary minus has already taken.
371
+ this.diagnostics.report(
372
+ "invalid_integer",
373
+ `${token.text} is outside the range of a 64-bit integer`,
374
+ token.start,
375
+ token.end,
376
+ );
377
+ this.stopped = true;
378
+ return this.unparsed(token.start, token.end);
379
+ }
380
+ return this.keep({
381
+ kind: "literal",
382
+ literal: { type: "int", value: token.int! },
383
+ range: [token.start, token.end],
384
+ });
385
+ case "uint":
386
+ this.advance();
387
+ return this.keep({
388
+ kind: "literal",
389
+ literal: { type: "uint", value: token.int! },
390
+ range: [token.start, token.end],
391
+ });
392
+ case "double":
393
+ this.advance();
394
+ return this.keep({
395
+ kind: "literal",
396
+ literal: { type: "double", value: token.double! },
397
+ range: [token.start, token.end],
398
+ });
399
+ case "string":
400
+ this.advance();
401
+ return this.keep({
402
+ kind: "literal",
403
+ literal: { type: "string", value: token.string! },
404
+ range: [token.start, token.end],
405
+ });
406
+ case "bytes":
407
+ this.advance();
408
+ return this.keep({
409
+ kind: "literal",
410
+ literal: { type: "bytes", value: token.bytes! },
411
+ range: [token.start, token.end],
412
+ });
413
+ case "reserved":
414
+ this.expectIdentifier(token);
415
+ return this.unparsed(token.start, token.end);
416
+ case "keyword":
417
+ // An operator written as a word, where an expression must begin.
418
+ this.unexpected(token);
419
+ return this.unparsed(token.start, token.end);
420
+ case "quotedIdent":
421
+ // A quoted name reads a MEMBER; nothing else in the language is written that way.
422
+ this.unexpected(token, "a member read, as in a.`b`");
423
+ return this.unparsed(token.start, token.end);
424
+ case "ident":
425
+ return this.word(token);
426
+ case "punct":
427
+ if (token.text === "(") return this.parenthesized(token);
428
+ if (token.text === "[") return this.list(token);
429
+ if (token.text === "{") return this.map(token);
430
+ if (token.text === ".") return this.absoluteName(token);
431
+ this.unexpected(token);
432
+ return this.unparsed(token.start, token.end);
433
+ case "eof":
434
+ this.unexpected(token);
435
+ return this.unparsed(token.start, token.end);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * `.y` — a name resolved against the environment alone. It is the only spelling for an
441
+ * outer name a comprehension variable of the same name would otherwise hide.
442
+ */
443
+ private absoluteName(dot: Token): CelNode {
444
+ this.advance();
445
+ const name = this.peek();
446
+ // A NAME, not a member: cel-spec's leading-dot form takes an identifier, so neither a
447
+ // reserved word nor a quoted name opens one.
448
+ if (!this.expectIdentifier(name)) return this.unparsed(dot.start, name.start);
449
+ this.advance();
450
+ return this.keep({ kind: "ident", name: name.text, absolute: true, range: [dot.start, name.end] });
451
+ }
452
+
453
+ private word(token: Token): CelNode {
454
+ this.advance();
455
+ const range: SourceRange = [token.start, token.end];
456
+ if (token.text === "true" || token.text === "false") {
457
+ return this.keep({ kind: "literal", literal: { type: "bool", value: token.text === "true" }, range });
458
+ }
459
+ if (token.text === "null") {
460
+ return this.keep({ kind: "literal", literal: { type: "null" }, range });
461
+ }
462
+ if (!this.isPunct("(")) return this.keep({ kind: "ident", name: token.text, absolute: false, range });
463
+ const args = this.callArguments();
464
+ return this.keep({
465
+ kind: "call",
466
+ name: token.text,
467
+ nameRange: range,
468
+ args: args.nodes,
469
+ range: [token.start, args.end],
470
+ });
471
+ }
472
+
473
+ private parenthesized(open: Token): CelNode {
474
+ this.advance();
475
+ const inner = this.conditional();
476
+ if (!this.stopped) this.expectPunct(")");
477
+ return inner;
478
+ }
479
+
480
+ /** The arguments of a call, the open paren being the current token. */
481
+ private callArguments(): { nodes: CelNode[]; end: number } {
482
+ const open = this.advance();
483
+ const nodes: CelNode[] = [];
484
+ while (!this.stopped && !this.isPunct(")")) {
485
+ const argument = this.conditional();
486
+ nodes.push(argument);
487
+ if (nodes.length > this.limits.maxCallArguments) {
488
+ this.limit("call arguments", this.limits.maxCallArguments, open.start, argument.range[1]);
489
+ break;
490
+ }
491
+ if (!this.takePunct(",")) break;
492
+ }
493
+ if (this.stopped) return { nodes, end: nodes.at(-1)?.range[1] ?? open.end };
494
+ const close = this.peek();
495
+ if (!this.expectPunct(")")) return { nodes, end: close.end };
496
+ return { nodes, end: close.end };
497
+ }
498
+
499
+ private list(open: Token): CelNode {
500
+ this.advance();
501
+ const elements: CelListElement[] = [];
502
+ while (!this.stopped && !this.isPunct("]")) {
503
+ const optional = this.optionalSyntax && this.takePunct("?");
504
+ const value = this.conditional();
505
+ elements.push({ value, optional });
506
+ if (elements.length > this.limits.maxListElements) {
507
+ this.limit("list elements", this.limits.maxListElements, open.start, value.range[1]);
508
+ break;
509
+ }
510
+ if (!this.takePunct(",")) break;
511
+ }
512
+ const end = this.closingEnd("]", open, elements.at(-1)?.value.range[1]);
513
+ return this.keep({ kind: "list", elements, range: [open.start, end] });
514
+ }
515
+
516
+ private map(open: Token): CelNode {
517
+ this.advance();
518
+ const entries: CelMapEntry[] = [];
519
+ while (!this.stopped && !this.isPunct("}")) {
520
+ const optional = this.optionalSyntax && this.takePunct("?");
521
+ const key = this.conditional();
522
+ if (this.stopped || !this.expectPunct(":")) {
523
+ entries.push({ key, value: this.unparsed(key.range[1]), optional });
524
+ break;
525
+ }
526
+ const value = this.conditional();
527
+ entries.push({ key, value, optional });
528
+ if (entries.length > this.limits.maxMapEntries) {
529
+ this.limit("map entries", this.limits.maxMapEntries, open.start, value.range[1]);
530
+ break;
531
+ }
532
+ if (!this.takePunct(",")) break;
533
+ }
534
+ const end = this.closingEnd("}", open, entries.at(-1)?.value.range[1]);
535
+ return this.keep({ kind: "map", entries, range: [open.start, end] });
536
+ }
537
+
538
+ /** Where an aggregate ends: at its closing bracket, or at what it did read. */
539
+ private closingEnd(bracket: string, open: Token, lastRead: number | undefined): number {
540
+ if (!this.stopped && this.expectPunct(bracket)) return this.tokens[this.at - 1]!.end;
541
+ return lastRead ?? open.end;
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Reads one CEL expression. Always answers a tree; `diagnostics` holds at most one
547
+ * entry, and holds one exactly when the whole source could not be read.
548
+ */
549
+ export function parseSyntax(source: string, options?: ParseOptions): ParseResult {
550
+ const limits = resolveParseLimits(options?.limits);
551
+ const { tokens, diagnostics } = tokenize(source);
552
+ const root = new Parser(source, tokens, diagnostics, limits, options?.optionalSyntax ?? false).parse();
553
+ return { source, root, diagnostics: diagnostics.list() };
554
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Every call an expression makes on a namespace.
3
+ *
4
+ * Only a resolved tree holds one (`namespace-resolution.ts`), so the answer is
5
+ * always relative to the namespace set the tree was resolved under. A consumer uses
6
+ * it to say which functions of which other modules an expression reaches, which
7
+ * cannot be re-derived from the expression alone — the name set is not in the text.
8
+ *
9
+ * The order is source order: a call before any call written inside it.
10
+ */
11
+
12
+ import type { CelNode, SourceRange } from "./syntax-tree.js";
13
+ import { walkTree } from "./syntax-tree.js";
14
+
15
+ export interface QualifiedCall {
16
+ readonly namespace: string;
17
+ readonly name: string;
18
+ /** `<namespace>.<name>`, the spelling the call was written with. */
19
+ readonly qualifiedName: string;
20
+ readonly arity: number;
21
+ readonly range: SourceRange;
22
+ readonly nameRange: SourceRange;
23
+ }
24
+
25
+ export function qualifiedCalls(root: CelNode): readonly QualifiedCall[] {
26
+ const calls: QualifiedCall[] = [];
27
+ for (const node of walkTree(root)) {
28
+ if (node.kind !== "qcall") continue;
29
+ calls.push({
30
+ namespace: node.namespace,
31
+ name: node.name,
32
+ qualifiedName: `${node.namespace}.${node.name}`,
33
+ arity: node.args.length,
34
+ range: node.range,
35
+ nameRange: node.nameRange,
36
+ });
37
+ }
38
+ return calls;
39
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `matches` — and it is RE2, not the host's regular expressions.
3
+ *
4
+ * CEL's pattern language is RE2: linear-time matching with no backreferences and no
5
+ * lookaround. The host's own engine accepts patterns RE2 refuses and is exponential on
6
+ * some of them, so an expression that matches here would behave differently on another
7
+ * runtime and a hostile pattern in a manifest would be a denial of service. A pattern
8
+ * RE2 cannot parse is `invalid_regular_expression`, never a silent `false`.
9
+ *
10
+ * Compilation is memoized, bounded, because a pattern is almost always a literal
11
+ * evaluated many times. **One compile serves every caller** — the language's `matches`
12
+ * and the catalog's regex family alike — so a pattern is read one way and the cache is
13
+ * not duplicated; what differs is only how each words its refusal, which is why a refusal
14
+ * answers RE2's parse-error KIND beside the library's whole message.
15
+ */
16
+
17
+ import { RE2JS, RE2JSSyntaxException } from "re2js";
18
+ import { BoundedCache } from "./bounded-cache.js";
19
+ import { celError, type CelError } from "./cel-value.js";
20
+ import type { SourceRange } from "./syntax-tree.js";
21
+
22
+ /** How many compiled patterns one process keeps. */
23
+ export const PATTERN_CACHE_CAPACITY = 128;
24
+
25
+ /** The flag letters a caller may map onto RE2's own flags. */
26
+ export const RE2_FLAG_LETTERS: Readonly<Record<string, number>> = {
27
+ i: RE2JS.CASE_INSENSITIVE,
28
+ m: RE2JS.MULTILINE,
29
+ s: RE2JS.DOTALL,
30
+ };
31
+
32
+ /**
33
+ * RE2's parse-error kinds, in RE2's own words — the closed vocabulary an invalid-pattern
34
+ * refusal ends with, so every engine words one identically. The last two are RE2's parser
35
+ * limits: a nesting height above 1000, and a parsed size above 3,355,443.
36
+ */
37
+ export const RE2_PATTERN_ERROR_KINDS: readonly string[] = [
38
+ "missing closing )",
39
+ "missing closing ]",
40
+ "unexpected )",
41
+ "trailing backslash at end of expression",
42
+ "invalid escape sequence",
43
+ "invalid character class range",
44
+ "invalid named capture",
45
+ "duplicate capture group name",
46
+ "invalid or unsupported Perl syntax",
47
+ "missing argument to repetition operator",
48
+ "invalid nested repetition operator",
49
+ "invalid repeat count",
50
+ "expression nests too deeply",
51
+ "expression too large",
52
+ ];
53
+
54
+ /**
55
+ * Why RE2 refused a pattern: its own parse-error kind where it reported one of the
56
+ * closed vocabulary above, and its whole message. A caller that prints the kind decides
57
+ * for itself what an absent one is — the language's `matches` prints the message, so a
58
+ * refusal RE2 words some other way is still an answer about the pattern there.
59
+ */
60
+ export interface RE2PatternRefusal {
61
+ readonly kind?: string;
62
+ readonly message: string;
63
+ }
64
+
65
+ export type RE2Compiled = { readonly pattern: RE2JS } | { readonly refused: RE2PatternRefusal };
66
+
67
+ const compiled = new BoundedCache<string, RE2Compiled>(PATTERN_CACHE_CAPACITY);
68
+
69
+ /** A compiled pattern, or why RE2 refused it. One compile and one cache for every caller. */
70
+ export function re2Pattern(pattern: string, flags = 0): RE2Compiled {
71
+ const key = `${flags}\u0000${pattern}`;
72
+ let held = compiled.get(key);
73
+ if (held === undefined) {
74
+ try {
75
+ held = { pattern: RE2JS.compile(pattern, flags) };
76
+ } catch (cause) {
77
+ const kind = cause instanceof RE2JSSyntaxException ? cause.error : undefined;
78
+ held = {
79
+ refused: {
80
+ ...(kind !== undefined && RE2_PATTERN_ERROR_KINDS.includes(kind) ? { kind } : {}),
81
+ message: (cause as Error).message,
82
+ },
83
+ };
84
+ }
85
+ compiled.set(key, held);
86
+ }
87
+ return held;
88
+ }
89
+
90
+ /** Whether the pattern matches anywhere in the text, or why the pattern is not one. */
91
+ export function celMatches(text: string, pattern: string, range?: SourceRange): boolean | CelError {
92
+ const held = re2Pattern(pattern);
93
+ if ("refused" in held) {
94
+ return celError(
95
+ "invalid_regular_expression",
96
+ `${JSON.stringify(pattern)} is not an RE2 pattern: ${held.refused.message}`,
97
+ range,
98
+ );
99
+ }
100
+ return held.pattern.matcher(text).find();
101
+ }