@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,197 @@
1
+ /**
2
+ * Typing the macros — the constructs that are written as calls and are not functions.
3
+ *
4
+ * A macro binds a name, or inspects the shape of its argument, so it cannot be a
5
+ * registered signature: `xs.map(i, i + 1)` has no argument type for `i`, and `has(a.b)`
6
+ * is about whether `b` is there rather than about its value. The parser deliberately
7
+ * leaves each one an ordinary call node (expanding it would make the source unwritable
8
+ * from the tree), so this is where the comprehension appears — in the checker's own
9
+ * lowering, which is also why a macro is **not** a dispatched call in the call listing.
10
+ *
11
+ * Which arguments a macro binds a name over is declared once, in
12
+ * `comprehension-bindings.ts`, and read both here and by the free-variable query. Two
13
+ * tables would be two answers to "what does this macro bind".
14
+ */
15
+
16
+ import type { CelType } from "./cel-type.js";
17
+ import { BOOL, DYN, formatType, isDyn, listOf, optionalOf, parameterOf } from "./cel-type.js";
18
+ import type { CelCheckCode } from "./check-diagnostic.js";
19
+ import { namespaceMacroBinding, receiverMacroBinding } from "./comprehension-bindings.js";
20
+ import type { CelNode, SourceRange } from "./syntax-tree.js";
21
+
22
+ /** What a macro needs of the checker around it. */
23
+ export interface MacroHost {
24
+ /** The type of a subexpression, in the scope that holds here. */
25
+ typeOf(node: CelNode): CelType;
26
+ /** The type of a subexpression with extra names in scope. */
27
+ typeOfBinding(node: CelNode, bindings: ReadonlyMap<string, CelType>): CelType;
28
+ /**
29
+ * The type of a select read as a question about PRESENCE — `has()`'s argument. It is
30
+ * the same reading `.?b` gets, which is what keeps the two forms of one question from
31
+ * answering differently about a union whose branches do not all hold the member.
32
+ */
33
+ typeOfPresence(node: Extract<CelNode, { kind: "select" }>): CelType;
34
+ report(code: CelCheckCode, message: string, range: SourceRange): void;
35
+ }
36
+
37
+ /** The reserved namespaces a macro is written on, and the macros on each. */
38
+ const NAMESPACE_MACROS: Readonly<Record<string, readonly string[]>> = {
39
+ cel: ["bind"],
40
+ optional: ["of", "none", "ofNonZeroValue"],
41
+ };
42
+
43
+ /** The optional library's two name-binding members, which no signature can state. */
44
+ const OPTIONAL_BINDING_MACROS = new Set(["optMap", "optFlatMap"]);
45
+
46
+ /** Whether a call is a macro at all — asked before any overload is looked for. */
47
+ export function isMacroCall(node: CelNode): boolean {
48
+ if (node.kind === "call") return node.name === "has" && node.args.length === 1;
49
+ if (node.kind !== "receiverCall") return false;
50
+ if (receiverMacroBinding(node.name, node.args.length)) return true;
51
+ const receiver = node.receiver;
52
+ if (receiver.kind !== "ident") return false;
53
+ const on = NAMESPACE_MACROS[receiver.name];
54
+ return on !== undefined && on.includes(node.name);
55
+ }
56
+
57
+ /**
58
+ * The type of a macro call. `isMacroCall` has already said it is one; anything this
59
+ * cannot type reports its own diagnostic and answers `dyn`, so one mistake stays one.
60
+ */
61
+ export function checkMacro(node: CelNode, host: MacroHost): CelType {
62
+ if (node.kind === "call") return checkHas(node, host);
63
+ if (node.kind !== "receiverCall") return DYN;
64
+ const receiver = node.receiver;
65
+ if (receiver.kind === "ident" && NAMESPACE_MACROS[receiver.name]) {
66
+ return receiver.name === "cel"
67
+ ? checkBind(node, host)
68
+ : checkOptionalNamespace(node, host);
69
+ }
70
+ if (OPTIONAL_BINDING_MACROS.has(node.name) && node.args.length === 2) {
71
+ return checkOptionalBinding(node, host);
72
+ }
73
+ return checkComprehension(node, host);
74
+ }
75
+
76
+ /**
77
+ * `opt.optMap(v, expr)` and `opt.optFlatMap(v, expr)`: the held value under a name, for
78
+ * one expression. `optMap` wraps what that expression answers; `optFlatMap` takes an
79
+ * optional from it and does not wrap it again.
80
+ */
81
+ function checkOptionalBinding(node: Extract<CelNode, { kind: "receiverCall" }>, host: MacroHost): CelType {
82
+ const receiver = host.typeOf(node.receiver);
83
+ const name = node.args[0]!;
84
+ if (name.kind !== "ident") return DYN;
85
+ let held: CelType = DYN;
86
+ if (receiver.kind === "optional") held = receiver.value;
87
+ else if (!isDyn(receiver) && receiver.kind !== "parameter") {
88
+ host.report(
89
+ "CEL_TYPE_ERROR",
90
+ `${node.name} reads an optional, and ${formatType(receiver)} is not one`,
91
+ node.receiver.range,
92
+ );
93
+ }
94
+ const result = host.typeOfBinding(node.args[1]!, new Map([[name.name, held]]));
95
+ if (node.name === "optMap") return optionalOf(result);
96
+ if (result.kind === "optional" || isDyn(result) || result.kind === "parameter") return result;
97
+ host.report(
98
+ "CEL_TYPE_ERROR",
99
+ `optFlatMap's expression must answer an optional, and it answers ${formatType(result)}`,
100
+ node.args[1]!.range,
101
+ );
102
+ return optionalOf(DYN);
103
+ }
104
+
105
+ /** `has(a.b)` — a question about presence. Its shape is already validated. */
106
+ function checkHas(node: Extract<CelNode, { kind: "call" }>, host: MacroHost): CelType {
107
+ const argument = node.args[0]!;
108
+ if (argument.kind === "select") host.typeOfPresence(argument);
109
+ return BOOL;
110
+ }
111
+
112
+ /** `cel.bind(name, value, body)` — a name for one value, in scope in the body alone. */
113
+ function checkBind(node: Extract<CelNode, { kind: "receiverCall" }>, host: MacroHost): CelType {
114
+ const binding = namespaceMacroBinding("cel", node.name, node.args.length);
115
+ if (!binding) {
116
+ host.report(
117
+ "CEL_INVALID_ARGUMENT",
118
+ "cel.bind(name, value, body) takes three arguments: a name, its value, and the expression that reads it",
119
+ node.range,
120
+ );
121
+ return DYN;
122
+ }
123
+ const name = node.args[binding.variableArgument]!;
124
+ if (name.kind !== "ident") return DYN;
125
+ const value = host.typeOf(node.args[1]!);
126
+ return host.typeOfBinding(node.args[2]!, new Map([[name.name, value]]));
127
+ }
128
+
129
+ /** `optional.of(v)`, `optional.ofNonZeroValue(v)` and `optional.none()`. */
130
+ function checkOptionalNamespace(node: Extract<CelNode, { kind: "receiverCall" }>, host: MacroHost): CelType {
131
+ if (node.name === "none") {
132
+ if (node.args.length !== 0) {
133
+ host.report("CEL_INVALID_ARGUMENT", "optional.none() takes no argument", node.range);
134
+ }
135
+ return optionalOf(parameterOf("T"));
136
+ }
137
+ if (node.args.length !== 1) {
138
+ host.report("CEL_INVALID_ARGUMENT", `optional.${node.name}(value) takes one argument`, node.range);
139
+ return optionalOf(DYN);
140
+ }
141
+ return optionalOf(host.typeOf(node.args[0]!));
142
+ }
143
+
144
+ /** `all`, `exists`, `exists_one`, `filter` and `map` over a list or a map. */
145
+ function checkComprehension(node: Extract<CelNode, { kind: "receiverCall" }>, host: MacroHost): CelType {
146
+ const binding = receiverMacroBinding(node.name, node.args.length)!;
147
+ const receiver = host.typeOf(node.receiver);
148
+ const element = iterationType(receiver, node, host);
149
+ const name = node.args[binding.variableArgument]!;
150
+ if (name.kind !== "ident") return DYN;
151
+ const bindings = new Map([[name.name, element]]);
152
+ const scoped = binding.scopedArguments.map((at) => ({
153
+ node: node.args[at]!,
154
+ type: host.typeOfBinding(node.args[at]!, bindings),
155
+ }));
156
+
157
+ if (node.name === "all" || node.name === "exists" || node.name === "exists_one") {
158
+ requireBool(scoped[0]!, node.name, host);
159
+ return BOOL;
160
+ }
161
+ if (node.name === "filter") {
162
+ requireBool(scoped[0]!, node.name, host);
163
+ return listOf(element);
164
+ }
165
+ // `map` in both arities: the last scoped argument is the transform, and a third
166
+ // argument makes the one before it a filter.
167
+ if (scoped.length === 2) requireBool(scoped[0]!, node.name, host);
168
+ return listOf(scoped.at(-1)!.type);
169
+ }
170
+
171
+ function requireBool(
172
+ scoped: { node: CelNode; type: CelType },
173
+ name: string,
174
+ host: MacroHost,
175
+ ): void {
176
+ if (isDyn(scoped.type) || scoped.type.kind === "parameter") return;
177
+ if (scoped.type.kind === "primitive" && scoped.type.name === "bool") return;
178
+ host.report(
179
+ "CEL_TYPE_ERROR",
180
+ `${name}'s test must be bool, and it is ${formatType(scoped.type)}`,
181
+ scoped.node.range,
182
+ );
183
+ }
184
+
185
+ /** What one element of a comprehension's receiver is: a list's element, a map's key. */
186
+ function iterationType(receiver: CelType, node: CelNode, host: MacroHost): CelType {
187
+ if (isDyn(receiver) || receiver.kind === "parameter") return DYN;
188
+ if (receiver.kind === "list") return receiver.element;
189
+ if (receiver.kind === "map") return receiver.key;
190
+ if (receiver.kind === "record") return { kind: "primitive", name: "string" };
191
+ host.report(
192
+ "CEL_TYPE_ERROR",
193
+ `a comprehension reads a list or a map, and ${formatType(receiver)} is neither`,
194
+ node.kind === "receiverCall" ? node.receiver.range : node.range,
195
+ );
196
+ return DYN;
197
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Whether each macro call is shaped like one at all, judged before any type is read.
3
+ *
4
+ * A macro's own arguments are not values: `xs.map(i, …)` declares the name `i`, and
5
+ * `has(a.b)` asks about the member `b` of a chain of names. Neither is something a type
6
+ * can be wrong about — they are either written in the shape the macro takes, or the call
7
+ * is not that macro. So this is a **structural** pass over the whole tree, and it runs
8
+ * first: a macro written wrongly is reported before anything inside it is typed, because
9
+ * the typing of its body depends on a name the call failed to declare.
10
+ *
11
+ * The walk is **post-order**, so the innermost mistake is the one reported first. Nesting
12
+ * macros is how a generated expression goes wrong, and the inner call is the one a reader
13
+ * has to fix.
14
+ */
15
+
16
+ import type { CelCheckCode } from "./check-diagnostic.js";
17
+ import { namespaceMacroBinding, receiverMacroBinding } from "./comprehension-bindings.js";
18
+ import type { CelNode, SourceRange } from "./syntax-tree.js";
19
+ import { childNodes } from "./syntax-tree.js";
20
+
21
+ export interface MacroShapeFinding {
22
+ readonly code: CelCheckCode;
23
+ readonly message: string;
24
+ readonly range: SourceRange;
25
+ }
26
+
27
+ /** Every macro call whose own arguments are not the shape it takes, innermost first. */
28
+ export function macroShapeFindings(root: CelNode): readonly MacroShapeFinding[] {
29
+ const findings: MacroShapeFinding[] = [];
30
+ collect(root, findings);
31
+ return findings;
32
+ }
33
+
34
+ function collect(node: CelNode, findings: MacroShapeFinding[]): void {
35
+ for (const child of childNodes(node)) collect(child, findings);
36
+ const finding = shapeOf(node);
37
+ if (finding) findings.push(finding);
38
+ }
39
+
40
+ function shapeOf(node: CelNode): MacroShapeFinding | undefined {
41
+ if (node.kind === "call" && node.name === "has" && node.args.length === 1) {
42
+ const argument = node.args[0]!;
43
+ if (argument.kind === "select" && argument.field !== "") return undefined;
44
+ return {
45
+ code: "CEL_INVALID_ARGUMENT",
46
+ message: "has() asks whether a member is there, so it takes a member read: write has(a.b)",
47
+ range: argument.range,
48
+ };
49
+ }
50
+ if (node.kind !== "receiverCall") return undefined;
51
+ const binding =
52
+ receiverMacroBinding(node.name, node.args.length) ??
53
+ namespaceMacroBinding(
54
+ node.receiver.kind === "ident" ? node.receiver.name : "",
55
+ node.name,
56
+ node.args.length,
57
+ );
58
+ if (!binding) return undefined;
59
+ const variable = node.args[binding.variableArgument]!;
60
+ if (variable.kind === "ident") return undefined;
61
+ return {
62
+ code: "CEL_INVALID_ARGUMENT",
63
+ message: `${node.name} binds a name here, and this is not a name`,
64
+ range: variable.range,
65
+ };
66
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The member-read seam: **one** lookup, for every form a read is written in.
3
+ *
4
+ * `a.b`, `a['b']`, `a[expr]`, `a.?b`, `a[?expr]`, a macro's field read and the presence
5
+ * question `has()` all resolve here, through a single lookup over the value's **own**
6
+ * entries. Neither backend ever performs a host property read, so no key — however it
7
+ * was computed — can reach a prototype, a method or a function property: `a['length']`
8
+ * on a map is a missing key, not `0`, and `a[request.query.k]` cannot reach
9
+ * `constructor`.
10
+ *
11
+ * That is why the reserved-word set holds CEL's own words and nothing else. A word list
12
+ * is the wrong instrument: a member *name* is written by the author, while a member
13
+ * *key* can come from a request, so no list at any position touches the form an
14
+ * attacker can actually reach — and refusing `__proto__` as a name would make a
15
+ * resource that may legally be called that unreadable through CEL.
16
+ *
17
+ * A key the value does not hold is the `no_such_key` **error value**, which
18
+ * participates in short-circuit. Never `undefined` passed along.
19
+ *
20
+ * **What each of the four verdicts answers is written once here**, in `lookupAbsence`,
21
+ * and read by both presence callers — so the closure backend, the emitter and the
22
+ * `has()` macro cannot differ by construction.
23
+ */
24
+
25
+ import { celMapKeys, mapKeyIdentity } from "./cel-map-value.js";
26
+ import type { CelValue } from "./cel-value.js";
27
+ import { celError, isCelMap, isCelRecord, isCelUint, type CelError } from "./cel-value.js";
28
+ import type { SourceRange } from "./syntax-tree.js";
29
+
30
+ /**
31
+ * What one lookup found. The four refusals are **symbols**, not objects: a lookup happens
32
+ * on nearly every evaluation, and a wrapper object per read is an allocation per read.
33
+ * `MISSING` is a key the container does not hold — which `has()` answers `false` for and a
34
+ * read turns into `no_such_key`; the other three are mistakes in the read itself.
35
+ */
36
+ export const MISSING = Symbol("cel.lookup.missing");
37
+ export const OUT_OF_RANGE = Symbol("cel.lookup.outOfRange");
38
+ export const UNSUPPORTED_CONTAINER = Symbol("cel.lookup.unsupportedContainer");
39
+ export const UNSUPPORTED_KEY = Symbol("cel.lookup.unsupportedKey");
40
+
41
+ export type Lookup =
42
+ | CelValue
43
+ | typeof MISSING
44
+ | typeof OUT_OF_RANGE
45
+ | typeof UNSUPPORTED_CONTAINER
46
+ | typeof UNSUPPORTED_KEY;
47
+
48
+ /** An int-valued index, or nothing when the value is of no index type. */
49
+ function indexOf(key: CelValue): bigint | undefined {
50
+ if (typeof key === "bigint") return key;
51
+ if (isCelUint(key)) return key.value;
52
+ // A double that holds a whole number indexes a list: a `dyn` arithmetic result is a
53
+ // double, and cel-spec indexes with it where it is integral.
54
+ if (typeof key === "number" && Number.isInteger(key)) return BigInt(key);
55
+ return undefined;
56
+ }
57
+
58
+ /**
59
+ * The single lookup. It is total over the value domain: a container that holds no members
60
+ * at all answers `UNSUPPORTED_CONTAINER` rather than falling back to a property read that
61
+ * might find `length`, `name`, `call` or `apply`.
62
+ */
63
+ export function celLookup(container: CelValue, key: CelValue): Lookup {
64
+ if (typeof key === "string" && isCelRecord(container)) {
65
+ // An OWN entry, never an inherited one: a host's object is data here.
66
+ return Object.prototype.hasOwnProperty.call(container, key)
67
+ ? (container[key] as CelValue)
68
+ : MISSING;
69
+ }
70
+ if (isCelMap(container)) {
71
+ const identity = mapKeyIdentity(key);
72
+ if (identity === undefined) return UNSUPPORTED_KEY;
73
+ const entry = container.entries.get(identity);
74
+ return entry ? entry.value : MISSING;
75
+ }
76
+ if (Array.isArray(container)) {
77
+ const at = indexOf(key);
78
+ if (at === undefined) return UNSUPPORTED_KEY;
79
+ if (at < 0n || at >= BigInt(container.length)) return OUT_OF_RANGE;
80
+ return container[Number(at)] as CelValue;
81
+ }
82
+ if (isCelRecord(container)) return UNSUPPORTED_KEY;
83
+ return UNSUPPORTED_CONTAINER;
84
+ }
85
+
86
+ /** The value at a key, or the error a failed lookup is. */
87
+ export function celRead(container: CelValue, key: CelValue, range?: SourceRange): CelValue | CelError {
88
+ const found = celLookup(container, key);
89
+ if (typeof found !== "symbol") return found;
90
+ return lookupError(found, key, range);
91
+ }
92
+
93
+ /**
94
+ * Whether a lookup that found nothing answers **absence** rather than an error. This is
95
+ * the seam's contract, in the one place both backends and the emitter read it: they all
96
+ * reach a member read through `backend-runtime.ts`, which reaches the four verdicts
97
+ * through here.
98
+ *
99
+ * - A **presence-shaped** read — `a.?b`, `a[?k]` and `has(a.b)` — answers absence for
100
+ * *missing*, *out of range* and *holds no members* alike, and the error only for an
101
+ * **unusable key**. It asks whether a member is THERE, and a value that cannot hold one
102
+ * has none to find. The authority is the optional library's own: it enters this engine
103
+ * from cel-go whole, and cel-go's attribute qualification answers "not found" for a
104
+ * receiver that is neither a mapper, a lister nor an indexer **whenever the read is a
105
+ * presence test**, erroring only otherwise — the error reading being an explicitly named
106
+ * opt-in (`EnableErrorOnBadPresenceTest`), which Telo does not carry, because a
107
+ * per-environment switch over what an expression MEANS would let the analyzer and a
108
+ * kernel disagree about one manifest.
109
+ * - A read **through a present optional, written in the ordinary form** — the `.c` of
110
+ * `a.?b.c` — is not a presence question about `c`. It answers absence for a key the held
111
+ * value does not hold (`optional.of({'c': {}}).c.missing` is absent, cel-spec's
112
+ * `optional_chaining_5`) and the error for a held value that holds no members at all
113
+ * (`{true: dyn(0)}[?true].absent` is cel-spec's error).
114
+ * - A **plain** read never asks: each of the four is the error `lookupError` words.
115
+ *
116
+ * An unusable key is the one refusal no form forgives: `[?3.1]` names an entry no
117
+ * container of that shape could hold, so it is a mistake in the READ rather than a member
118
+ * that happens to be absent.
119
+ */
120
+ export function lookupAbsence(found: symbol, presence: boolean): boolean {
121
+ if (found === MISSING || found === OUT_OF_RANGE) return true;
122
+ return presence && found === UNSUPPORTED_CONTAINER;
123
+ }
124
+
125
+ /** The error a refused lookup is. */
126
+ export function lookupError(found: symbol, key: CelValue, range?: SourceRange): CelError {
127
+ if (found === MISSING) return celError("no_such_key", `no such key: ${describe(key)}`, range);
128
+ if (found === OUT_OF_RANGE) {
129
+ return celError("index_out_of_range", `index out of range: ${describe(key)}`, range);
130
+ }
131
+ if (found === UNSUPPORTED_KEY) {
132
+ return celError("unsupported_key_type", `${describe(key)} cannot name an entry of this value`, range);
133
+ }
134
+ return celError("unsupported_container", "this value holds no members", range);
135
+ }
136
+
137
+ /**
138
+ * Whether a key is there — `has(a.b)`. The question is presence-shaped, so a value that
139
+ * holds no members answers `false` exactly as a missing key does: it has no member to
140
+ * find. Only an unusable key is a mistake in the question itself.
141
+ */
142
+ export function celHas(container: CelValue, key: CelValue, range?: SourceRange): boolean | CelError {
143
+ const found = celLookup(container, key);
144
+ if (typeof found !== "symbol") return true;
145
+ if (lookupAbsence(found, true)) return false;
146
+ return lookupError(found, key, range);
147
+ }
148
+
149
+ /**
150
+ * The elements a comprehension ranges over: a list's items, a map's keys. A list is handed
151
+ * over as it is — nothing in CEL mutates a value, and every comprehension reads its range —
152
+ * so copying it would cost the length of the list per comprehension to defend against a
153
+ * write no expression can perform.
154
+ */
155
+ export function celIterable(container: CelValue, range?: SourceRange): readonly CelValue[] | CelError {
156
+ if (Array.isArray(container)) return container as readonly CelValue[];
157
+ if (isCelMap(container)) return celMapKeys(container);
158
+ if (isCelRecord(container)) return Object.keys(container);
159
+ return celError("unsupported_container", "a comprehension reads a list or a map", range);
160
+ }
161
+
162
+ function describe(key: CelValue): string {
163
+ if (typeof key === "string") return JSON.stringify(key);
164
+ if (isCelUint(key)) return `${key.value}`;
165
+ if (typeof key === "bigint") return `${key}`;
166
+ return String(key);
167
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Turning `Alias.fn(x)` into a qualified call.
3
+ *
4
+ * `Alias.fn(x)` and `obj.method(x)` are **the same syntax** — a call written on a
5
+ * receiver — so no parser can tell them apart, and only a set of names that denote
6
+ * namespaces rather than values can. That set comes from the host, which is why
7
+ * this is a separate total tree-to-tree pass rather than a parser rule: the parser
8
+ * has one input and this has two.
9
+ *
10
+ * The pass is total — it visits every node and rewrites every receiver call whose
11
+ * receiver is a bare identifier in the set, wherever it sits. It is also the only
12
+ * producer of `qcall`, and `cel` and `optional` can never be in the set, because
13
+ * the standard macros are written on them (`cel.bind(…)`, `optional.of(…)`) and a
14
+ * namespace would capture those calls instead.
15
+ *
16
+ * A resolved tree records the set it was resolved under, so a consumer given a tree
17
+ * can tell whether it was resolved under the set it is about to check it against —
18
+ * the alternative is a tree that looks resolved and silently is not.
19
+ */
20
+
21
+ import { isIdentifierSpelling, isReservedWord } from "./reserved-words.js";
22
+ import type { CelMapEntry, CelNode } from "./syntax-tree.js";
23
+
24
+ /** Names that can never be registered as a namespace. */
25
+ export const RESERVED_NAMESPACES: readonly string[] = ["cel", "optional"];
26
+
27
+ /** A namespace set a host cannot have: the name is not a name, or is reserved. */
28
+ export class CelNamespaceError extends Error {
29
+ constructor(message: string) {
30
+ super(message);
31
+ this.name = "CelNamespaceError";
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Validates a namespace set and puts it in a canonical order, so that two sets with
37
+ * the same names compare equal however the host listed them.
38
+ */
39
+ export function normalizeNamespaces(names: Iterable<string>): readonly string[] {
40
+ const normalized: string[] = [];
41
+ for (const name of names) {
42
+ if (!isIdentifierSpelling(name)) {
43
+ throw new CelNamespaceError(`${JSON.stringify(name)} is not spelled as a name, so it names no namespace`);
44
+ }
45
+ if (isReservedWord(name)) {
46
+ throw new CelNamespaceError(`${JSON.stringify(name)} is a reserved word, so it names no namespace`);
47
+ }
48
+ if (RESERVED_NAMESPACES.includes(name)) {
49
+ throw new CelNamespaceError(
50
+ `${JSON.stringify(name)} is reserved: the standard macros are written on it, and a namespace would capture them`,
51
+ );
52
+ }
53
+ if (!normalized.includes(name)) normalized.push(name);
54
+ }
55
+ return normalized.sort();
56
+ }
57
+
58
+ export function namespaceSetsEqual(left: readonly string[], right: readonly string[]): boolean {
59
+ return left.length === right.length && left.every((name, at) => name === right[at]);
60
+ }
61
+
62
+ /** Rewrites every call on a name of the set into a `qcall`. Total; shares what it does not change. */
63
+ export function resolveNamespaces(root: CelNode, namespaces: readonly string[]): CelNode {
64
+ // With no namespaces the pass is the identity — nothing can match an empty set — so it
65
+ // answers without walking. Every site with no module names takes this path, which is why
66
+ // it is worth stating rather than leaving to the structural sharing below.
67
+ if (namespaces.length === 0) return root;
68
+ const resolved = mapChildren(root, (child) => resolveNamespaces(child, namespaces));
69
+ if (resolved.kind !== "receiverCall") return resolved;
70
+ const receiver = resolved.receiver;
71
+ // An absolute name denotes a value the environment declares, never a namespace: a
72
+ // namespaced call has exactly one spelling, and `.Alias.fn(x)` is not it.
73
+ if (receiver.kind !== "ident" || receiver.absolute || !namespaces.includes(receiver.name)) {
74
+ return resolved;
75
+ }
76
+ return {
77
+ kind: "qcall",
78
+ namespace: receiver.name,
79
+ namespaceRange: receiver.range,
80
+ name: resolved.name,
81
+ nameRange: resolved.nameRange,
82
+ args: resolved.args,
83
+ range: resolved.range,
84
+ };
85
+ }
86
+
87
+ /** Rebuilds a node from its mapped children, returning the original when none moved. */
88
+ function mapChildren(node: CelNode, map: (child: CelNode) => CelNode): CelNode {
89
+ switch (node.kind) {
90
+ case "literal":
91
+ case "ident":
92
+ case "unparsed":
93
+ return node;
94
+ case "list": {
95
+ let moved = false;
96
+ const elements = node.elements.map((element) => {
97
+ const value = map(element.value);
98
+ if (value === element.value) return element;
99
+ moved = true;
100
+ return { ...element, value };
101
+ });
102
+ return moved ? { ...node, elements } : node;
103
+ }
104
+ case "map": {
105
+ let moved = false;
106
+ const entries: CelMapEntry[] = node.entries.map((entry) => {
107
+ const key = map(entry.key);
108
+ const value = map(entry.value);
109
+ if (key === entry.key && value === entry.value) return entry;
110
+ moved = true;
111
+ return { ...entry, key, value };
112
+ });
113
+ return moved ? { ...node, entries } : node;
114
+ }
115
+ case "select": {
116
+ const operand = map(node.operand);
117
+ return operand === node.operand ? node : { ...node, operand };
118
+ }
119
+ case "index": {
120
+ const operand = map(node.operand);
121
+ const index = map(node.index);
122
+ return operand === node.operand && index === node.index ? node : { ...node, operand, index };
123
+ }
124
+ case "call":
125
+ case "qcall": {
126
+ const args = mapList(node.args, map);
127
+ return args ? { ...node, args } : node;
128
+ }
129
+ case "receiverCall": {
130
+ const receiver = map(node.receiver);
131
+ const args = mapList(node.args, map);
132
+ if (receiver === node.receiver && !args) return node;
133
+ return { ...node, receiver, args: args ?? node.args };
134
+ }
135
+ case "unary": {
136
+ const operand = map(node.operand);
137
+ return operand === node.operand ? node : { ...node, operand };
138
+ }
139
+ case "binary": {
140
+ const left = map(node.left);
141
+ const right = map(node.right);
142
+ return left === node.left && right === node.right ? node : { ...node, left, right };
143
+ }
144
+ case "conditional": {
145
+ const condition = map(node.condition);
146
+ const whenTrue = map(node.whenTrue);
147
+ const whenFalse = map(node.whenFalse);
148
+ return condition === node.condition && whenTrue === node.whenTrue && whenFalse === node.whenFalse
149
+ ? node
150
+ : { ...node, condition, whenTrue, whenFalse };
151
+ }
152
+ }
153
+ }
154
+
155
+ /** The mapped list, or nothing when every element is the one it started as. */
156
+ function mapList(
157
+ nodes: readonly CelNode[],
158
+ map: (child: CelNode) => CelNode,
159
+ ): readonly CelNode[] | undefined {
160
+ let moved = false;
161
+ const mapped = nodes.map((node) => {
162
+ const next = map(node);
163
+ if (next !== node) moved = true;
164
+ return next;
165
+ });
166
+ return moved ? mapped : undefined;
167
+ }