@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/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@telorun/cel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.108.0",
4
+ "description": "The Common Expression Language for Telo: read, type-check and evaluate an expression, hold it as a tree, write it back.",
5
+ "keywords": [
6
+ "telo",
7
+ "cel",
8
+ "expression",
9
+ "parser"
10
+ ],
11
+ "author": "Bartosz Pasiński <bartosz.pasinski@codenet.pl>",
12
+ "license": "SEE LICENSE IN LICENSE",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/telorun/telo.git",
16
+ "directory": "cel/nodejs"
17
+ },
18
+ "homepage": "https://github.com/telorun/telo#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/telorun/telo/issues"
21
+ },
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "exports": {
25
+ ".": {
26
+ "source": "./src/index.ts",
27
+ "types": "./dist/index.d.ts",
28
+ "bun": "./src/index.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist/**",
36
+ "src/**"
37
+ ],
38
+ "dependencies": {
39
+ "d3-format": "3.1.2",
40
+ "re2js": "2.8.3",
41
+ "uuid": "14.0.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/d3-format": "^3.0.4",
45
+ "@types/node": "^20.0.0",
46
+ "esbuild": "^0.25.12",
47
+ "typescript": "^5.0.0",
48
+ "vitest": "^2.1.8"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.lib.json",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "test:conformance": "vitest run --config vitest.conformance.config.ts",
55
+ "check:types": "tsc -p tsconfig.json --noEmit",
56
+ "check:browser-safe": "node scripts/check-browser-safe.mjs",
57
+ "check:signatures": "pnpm run build && node scripts/check-signatures.mjs"
58
+ }
6
59
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The activation: the values an expression reads names from.
3
+ *
4
+ * Two rules, both of which the checker already applies to types, so the evaluator must
5
+ * apply them to values or `telo check` and the runtime disagree:
6
+ *
7
+ * - **A dotted declaration is one name, and the longest one wins.** A host may hold
8
+ * `a.b.c`, or `a.b` holding a map, or both; `a.b.c` reads the entry of that name where
9
+ * it is held and the map's entry where only `a.b` is. So resolution tries the longest
10
+ * prefix first, exactly as `qualifiedVariableType` does at check.
11
+ * - **A read is an OWN entry**, never an inherited one, so an activation whose prototype
12
+ * is `Object.prototype` cannot answer `constructor` or `toString`.
13
+ *
14
+ * A name nothing holds is `no_such_variable` — the error value, so it short-circuits like
15
+ * any other. A value read from here is a HOST's, so it is admitted through the backend's
16
+ * one entry point for a host value, which is where a thenable is refused.
17
+ */
18
+
19
+
20
+
21
+ /** What a host binds names to. A value is read through the guard below, never trusted. */
22
+ export interface CelActivation {
23
+ readonly [name: string]: unknown;
24
+ }
25
+
26
+ /**
27
+ * Whether the activation holds a name itself. The backend asks this for each prefix of a
28
+ * dotted chain, longest first, having built the prefixes once at compile time.
29
+ */
30
+ export function activationHolds(activation: CelActivation, name: string): boolean {
31
+ return Object.prototype.hasOwnProperty.call(activation, name);
32
+ }
@@ -0,0 +1,604 @@
1
+ /**
2
+ * What a backend does at each kind of site — once, for both of them.
3
+ *
4
+ * `runtime-library.ts` holds what every operator and standard function **does**. This
5
+ * holds everything *around* a call: admitting a host value, reading a member in every
6
+ * form, taking a bool operand, reading a name or a dotted chain, and the per-call-site
7
+ * overload dispatch with its bounded cache. None of it is a tree walk and none of it is an
8
+ * operator, which is exactly why it cannot live in a backend: the closure backend compiles
9
+ * a tree to closures and the emitter compiles the same tree to JavaScript source, and both
10
+ * call *these* functions. A second copy of the dispatch rule or the member-read rule is a
11
+ * second set of answers, and the whole point of two backends over one runtime is that
12
+ * there is one answer per operation.
13
+ *
14
+ * So the emitter emits a call to `readThrough` and never a bare `obj.name`; it emits a
15
+ * `CallSite` per call and never its own overload search; and a thenable is refused at the
16
+ * same doors on both backends because the doors are here.
17
+ */
18
+
19
+ import type { CelActivation } from "./activation.js";
20
+ import { activationHolds } from "./activation.js";
21
+ import { BoundedCache } from "./bounded-cache.js";
22
+ import type { CelType } from "./cel-type.js";
23
+ import {
24
+ BOOL,
25
+ BYTES,
26
+ DOUBLE,
27
+ DURATION,
28
+ DYN,
29
+ INT,
30
+ listOf,
31
+ mapOf,
32
+ NULL,
33
+ optionalOf,
34
+ STRING,
35
+ TIMESTAMP,
36
+ TYPE,
37
+ UINT,
38
+ } from "./cel-type.js";
39
+ import type { CelError, CelOptional, CelValue } from "./cel-value.js";
40
+ import {
41
+ asyncValueRefused,
42
+ CEL_VALUE_TYPE,
43
+ celError,
44
+ celNone,
45
+ celSome,
46
+ celTypeNameOf,
47
+ isCelError,
48
+ isCelOptional,
49
+ } from "./cel-value.js";
50
+ import type { FunctionRegistry, Resolution, ResolutionFailure } from "./function-registry.js";
51
+ import { celHas, celLookup, celRead, lookupAbsence, lookupError } from "./member-read.js";
52
+ import type { CelCallContext, CelImplementation } from "./runtime-library.js";
53
+ import { implementationOf } from "./runtime-library.js";
54
+ import type { CallForm } from "./signature.js";
55
+ import type { CelNode, CelSelectNode, SourceRange } from "./syntax-tree.js";
56
+
57
+ /** A compile-time refusal: a tree neither backend can compile at all. */
58
+ export class CelCompileError extends Error {
59
+ constructor(message: string) {
60
+ super(message);
61
+ this.name = "CelCompileError";
62
+ }
63
+ }
64
+
65
+ /** How many distinct argument-type combinations one call site remembers. */
66
+ export const CALL_SITE_CACHE_CAPACITY = 16;
67
+
68
+ /** What a namespaced call is dispatched through, when something bound it. */
69
+ /**
70
+ * A function bound under a namespace. Its arguments arrive as **one array**, where a
71
+ * registered overload's arrive positionally: a namespace declaration may withhold its
72
+ * parameter list entirely, so the count is the host's and not the dispatch key's. That is
73
+ * the whole rule — positional where the key fixes the arity, one array where the author
74
+ * decides it.
75
+ */
76
+ export type NamespaceImplementation = (args: readonly CelValue[], ctx: CelCallContext) => CelValue;
77
+
78
+ export type NamespaceDispatch = (
79
+ namespace: string,
80
+ name: string,
81
+ ) => NamespaceImplementation | undefined;
82
+
83
+ export interface EvaluationFrame {
84
+ readonly activation: CelActivation;
85
+ readonly slots: CelValue[];
86
+ /** Absent rather than optional: one frame shape, however the evaluation was started. */
87
+ readonly namespaceFunction: NamespaceDispatch | undefined;
88
+ }
89
+
90
+ /**
91
+ * One evaluation of one expression. The closure backend builds these by composition and
92
+ * the emitter's module returns them already built, so a program is indifferent to which
93
+ * backend produced it.
94
+ */
95
+ export type CelStep = (frame: EvaluationFrame) => CelValue;
96
+
97
+ /** What a backend needs of the environment it compiles against. */
98
+ export interface CompileTarget {
99
+ readonly registry: FunctionRegistry;
100
+ /** The library's own names — the type values and `google`. */
101
+ readonly constants: ReadonlyMap<string, CelValue>;
102
+ /** How many type arguments a host's named type takes, for dispatch on one of its values. */
103
+ readonly nominalArity: (name: string) => number | undefined;
104
+ /**
105
+ * Whether a host DECLARED this name — including a dotted one. It decides where a chain
106
+ * splits, at compile time, through the same function the checker uses.
107
+ */
108
+ readonly declares: (name: string) => boolean;
109
+ }
110
+
111
+ // --- host values -----------------------------------------------------------
112
+
113
+ /**
114
+ * A host value entering the engine — from an activation, from an implementation the host
115
+ * registered, or out of a host value a read reached into. A thenable is refused here rather
116
+ * than carried: an awaiting expression is an invocation in disguise, invisible to a journal
117
+ * and absent from a trace. The refusal itself lives in `cel-value.ts`, so every door answers
118
+ * with the same code and the same wording.
119
+ */
120
+ export function readHostValue(value: unknown, range: SourceRange): CelValue {
121
+ return asyncValueRefused(value, range) ?? (value as CelValue);
122
+ }
123
+
124
+ // --- names -----------------------------------------------------------------
125
+
126
+ /** A bare name: the activation's own entry, then the library's constants. */
127
+ export function readName(
128
+ activation: CelActivation,
129
+ constants: ReadonlyMap<string, CelValue>,
130
+ name: string,
131
+ range: SourceRange,
132
+ ): CelValue {
133
+ if (activationHolds(activation, name)) return readHostValue(activation[name], range);
134
+ const held = constants.get(name);
135
+ if (held !== undefined) return held;
136
+ return celError("no_such_variable", `no such variable: ${name}`, range);
137
+ }
138
+
139
+ /**
140
+ * One name read from the activation (or the library's constants), then its members. This
141
+ * is a chain whose split is already decided — at compile time, over the names the host
142
+ * declared (`declared-chain.ts`) — so evaluating it is one lookup plus member reads.
143
+ */
144
+ export function readNameChain(
145
+ activation: CelActivation,
146
+ constants: ReadonlyMap<string, CelValue>,
147
+ name: string,
148
+ rest: readonly string[],
149
+ range: SourceRange,
150
+ ): CelValue {
151
+ let held: CelValue;
152
+ if (activationHolds(activation, name)) held = readHostValue(activation[name], range);
153
+ else {
154
+ const constant = constants.get(name);
155
+ if (constant === undefined) return celError("no_such_variable", `no such variable: ${name}`, range);
156
+ held = constant;
157
+ }
158
+ for (const field of rest) {
159
+ if (isCelError(held)) return held;
160
+ held = readThrough(held, field, false, range);
161
+ }
162
+ return held;
163
+ }
164
+
165
+ /** One prefix of a dotted chain, and the segments left to read off its value. */
166
+ export interface ChainCandidate {
167
+ readonly name: string;
168
+ readonly rest: readonly string[];
169
+ }
170
+
171
+ /** Every prefix of a dotted chain, longest first, with the segments left to read. */
172
+ export function prefixCandidates(segments: readonly string[]): readonly ChainCandidate[] {
173
+ const candidates: ChainCandidate[] = [];
174
+ for (let length = segments.length; length >= 1; length -= 1) {
175
+ candidates.push({ name: segments.slice(0, length).join("."), rest: segments.slice(length) });
176
+ }
177
+ return candidates;
178
+ }
179
+
180
+ /**
181
+ * A chain **no** prefix of which the host declared: the activation is searched, longest
182
+ * prefix first. Every conformance row that binds a dotted key reads this way, and the
183
+ * checker has no opinion about such a chain either.
184
+ */
185
+ export function searchNameChain(
186
+ activation: CelActivation,
187
+ constants: ReadonlyMap<string, CelValue>,
188
+ candidates: readonly ChainCandidate[],
189
+ written: string,
190
+ range: SourceRange,
191
+ ): CelValue {
192
+ for (let at = 0; at < candidates.length; at += 1) {
193
+ const candidate = candidates[at]!;
194
+ if (!activationHolds(activation, candidate.name) && !constants.has(candidate.name)) continue;
195
+ return readNameChain(activation, constants, candidate.name, candidate.rest, range);
196
+ }
197
+ return celError("no_such_variable", `no such variable: ${written}`, range);
198
+ }
199
+
200
+ /**
201
+ * The dotted chain a select spells, when every step is a plain named member of a **free**
202
+ * name. A name a macro bound is a value, so a chain rooted at one is an ordinary member
203
+ * read — the same rule the checker applies, and the same answer both backends compile.
204
+ */
205
+ export function plainMemberChain(
206
+ node: CelSelectNode,
207
+ bound: (name: string) => boolean,
208
+ ): readonly string[] | undefined {
209
+ const segments: string[] = [];
210
+ let at: CelNode = node;
211
+ while (at.kind === "select") {
212
+ if (at.optional || at.field === "") return undefined;
213
+ segments.unshift(at.field);
214
+ at = at.operand;
215
+ }
216
+ if (at.kind !== "ident") return undefined;
217
+ if (!at.absolute && bound(at.name)) return undefined;
218
+ segments.unshift(at.name);
219
+ return segments;
220
+ }
221
+
222
+ // --- member reads ----------------------------------------------------------
223
+
224
+ /**
225
+ * A member read in every form. Reading **through an optional** answers an optional
226
+ * whichever form the read is written in, which is what lets a chain over a value that
227
+ * may be absent stay one expression: an absent one propagates as absent, and a key the
228
+ * held value does not have is absent too.
229
+ *
230
+ * Which refusals are absence and which are a mistake is `lookupAbsence`'s, and it turns
231
+ * on the FORM the read is written in rather than on what the operand turned out to be: a
232
+ * presence-shaped read over a value that holds no members is absent, while the ordinary
233
+ * read of a member of such a value is the mistake it is outside an optional too.
234
+ */
235
+ export function readThrough(
236
+ container: CelValue,
237
+ key: CelValue,
238
+ optionalForm: boolean,
239
+ range: SourceRange,
240
+ ): CelValue {
241
+ // The shape nearly every read has — a record's own string-keyed entry — decided by asking
242
+ // what the container is ONCE. The general path below is three total steps, so it reads the
243
+ // type key twice and the prototype again; this asks each question once and falls through
244
+ // to the one seam for every other container, every other key and every refusal.
245
+ if (!optionalForm && typeof key === "string" && typeof container === "object" && container !== null) {
246
+ if ((container as { [CEL_VALUE_TYPE]?: unknown })[CEL_VALUE_TYPE] === undefined) {
247
+ const prototype = Object.getPrototypeOf(container) as object | null;
248
+ if (
249
+ (prototype === Object.prototype || prototype === null) &&
250
+ Object.prototype.hasOwnProperty.call(container, key)
251
+ ) {
252
+ const held = (container as Record<string, CelValue>)[key] as CelValue;
253
+ // The thenable door, inline: `readHostValue` is this test and nothing else.
254
+ if (typeof held !== "object" || held === null || typeof (held as { then?: unknown }).then !== "function") {
255
+ return held;
256
+ }
257
+ }
258
+ }
259
+ }
260
+ if (isCelOptional(container)) {
261
+ if (!container.present) return celNone();
262
+ return optionalRead(container.held as CelValue, key, optionalForm, range);
263
+ }
264
+ if (optionalForm) return optionalRead(container, key, true, range);
265
+ // **A member read is a door a host value comes through**, and the value it answers is
266
+ // whatever the host put inside its own object: `readHostValue` is what refuses a thenable
267
+ // there, rather than the aggregate, the operator or the exit that happens to see it next.
268
+ return readHostValue(celRead(container, key, range), range);
269
+ }
270
+
271
+ /**
272
+ * A read that answers an optional. `presence` is whether the read was WRITTEN in a
273
+ * presence-shaped form (`.?`, `[?]`), which is what decides whether a value holding no
274
+ * members is absence or a mistake.
275
+ */
276
+ function optionalRead(
277
+ container: CelValue,
278
+ key: CelValue,
279
+ presence: boolean,
280
+ range: SourceRange,
281
+ ): CelValue {
282
+ const found = celLookup(container, key);
283
+ if (typeof found !== "symbol") {
284
+ const held = readHostValue(found, range);
285
+ // A value that must be awaited is refused rather than carried as a present optional.
286
+ return isCelError(held) ? held : celSome(held);
287
+ }
288
+ if (lookupAbsence(found, presence)) return celNone();
289
+ return lookupError(found, key, range);
290
+ }
291
+
292
+ /**
293
+ * `has(a.b)` — presence, which a missing key and a value that holds no members both
294
+ * answer `false` for rather than erroring. An absent optional has no members, and a
295
+ * present one is asked about what it holds.
296
+ */
297
+ export function hasMember(container: CelValue, field: CelValue, range: SourceRange): CelValue {
298
+ if (isCelOptional(container)) {
299
+ return container.present ? celHas(container.held as CelValue, field, range) : false;
300
+ }
301
+ return celHas(container, field, range);
302
+ }
303
+
304
+ // --- operands --------------------------------------------------------------
305
+
306
+ /** A bool operand, or the error it is — a non-bool is a mistake of its own. */
307
+ export function boolOperand(value: CelValue, range: SourceRange): boolean | CelError {
308
+ if (typeof value === "boolean") return value;
309
+ if (isCelError(value)) return value;
310
+ return celError("no_matching_overload", "this value is not a bool", range);
311
+ }
312
+
313
+ /** What an optional entry of an aggregate contributes, or the error it is not one. */
314
+ export function optionalEntry(value: CelValue, range: SourceRange): CelOptional | CelError {
315
+ if (isCelOptional(value)) return value;
316
+ return celError("no_matching_overload", "an entry written with '?' holds an optional", range);
317
+ }
318
+
319
+ // --- one call site ---------------------------------------------------------
320
+
321
+ /**
322
+ * What a call site resolved to: the behaviour, and whether it came from the HOST. The
323
+ * engine's own implementations are typed and cannot answer a thenable, so only a host's
324
+ * answer is checked for one — which keeps the check off the path every operator takes.
325
+ */
326
+ interface Dispatch {
327
+ readonly implementation: CelImplementation;
328
+ readonly foreign: boolean;
329
+ }
330
+
331
+ function dispatchOf(name: string, resolution: Resolution | ResolutionFailure): Dispatch | null {
332
+ if ("resolved" in resolution) {
333
+ const host = resolution.resolved.metadata.implementation;
334
+ if (host) return { implementation: host, foreign: true };
335
+ const own = implementationOf(resolution.resolved.signature);
336
+ return own ? { implementation: own, foreign: false } : null;
337
+ }
338
+ // **Equality is universal at runtime**, over every pair of types: the checker refuses
339
+ // `1 == 1u` deliberately, but `dyn(1) == 1u` reaches evaluation and cel-spec answers
340
+ // `true` there. A host's own registration still wins, because it resolved first.
341
+ const equality = equalityFallback(name);
342
+ return equality ? { implementation: equality, foreign: false } : null;
343
+ }
344
+
345
+ /** The universal equality, for a pair of types no registration names. */
346
+ function equalityFallback(name: string): CelImplementation | null {
347
+ if (name !== "==" && name !== "!=") return null;
348
+ return implementationOf({ name, form: "global", parameters: [], returns: DYN }) ?? null;
349
+ }
350
+
351
+ /**
352
+ * One call site, and the overloads it has resolved.
353
+ *
354
+ * **Overloads are resolved on the values' own types**, per site: `dyn(1.0) == 1` checks and
355
+ * must then answer across the numeric types, so the statically resolved signature is not
356
+ * enough. A site is almost always monomorphic, so the last resolution is held beside a
357
+ * bounded cache and reached by comparing the type names themselves — building a cache key
358
+ * per call is the allocation that costs most on the hottest path there is. A container's
359
+ * element type is read as `dyn` rather than walked, so dispatch does not get more expensive
360
+ * as the data gets larger.
361
+ *
362
+ * The arguments arrive **already evaluated and already free of errors**: carrying an
363
+ * error-valued operand out is the caller's, because only the caller knows how far it got.
364
+ */
365
+ export class CallSite {
366
+ private readonly resolved = new BoundedCache<string, Dispatch | null>(CALL_SITE_CACHE_CAPACITY);
367
+ private readonly context: CelCallContext;
368
+ /** The arity the last resolution was made under; `-1` until there is one. */
369
+ private count = -1;
370
+ private t0: string | undefined;
371
+ private t1: string | undefined;
372
+ private t2: string | undefined;
373
+ private t3: string | undefined;
374
+ private lastDispatch: Dispatch | null = null;
375
+
376
+ constructor(
377
+ private readonly name: string,
378
+ private readonly form: CallForm,
379
+ private readonly range: SourceRange,
380
+ private readonly registry: FunctionRegistry,
381
+ private readonly nominalArity: (name: string) => number | undefined,
382
+ ) {
383
+ this.context = { range };
384
+ }
385
+
386
+ call0(): CelValue {
387
+ if (this.count === 0) return this.answer(this.lastDispatch);
388
+ return this.resolve(0, undefined, undefined, undefined, undefined);
389
+ }
390
+
391
+ call1(a: CelValue): CelValue {
392
+ if (this.count === 1 && celTypeNameOf(a) === this.t0) return this.answer(this.lastDispatch, a);
393
+ return this.resolve(1, a, undefined, undefined, undefined);
394
+ }
395
+
396
+ call2(a: CelValue, b: CelValue): CelValue {
397
+ if (this.count === 2 && celTypeNameOf(a) === this.t0 && celTypeNameOf(b) === this.t1) {
398
+ return this.answer(this.lastDispatch, a, b);
399
+ }
400
+ return this.resolve(2, a, b, undefined, undefined);
401
+ }
402
+
403
+ call3(a: CelValue, b: CelValue, c: CelValue): CelValue {
404
+ if (
405
+ this.count === 3 &&
406
+ celTypeNameOf(a) === this.t0 &&
407
+ celTypeNameOf(b) === this.t1 &&
408
+ celTypeNameOf(c) === this.t2
409
+ ) {
410
+ return this.answer(this.lastDispatch, a, b, c);
411
+ }
412
+ return this.resolve(3, a, b, c, undefined);
413
+ }
414
+
415
+ call4(a: CelValue, b: CelValue, c: CelValue, d: CelValue): CelValue {
416
+ if (
417
+ this.count === 4 &&
418
+ celTypeNameOf(a) === this.t0 &&
419
+ celTypeNameOf(b) === this.t1 &&
420
+ celTypeNameOf(c) === this.t2 &&
421
+ celTypeNameOf(d) === this.t3
422
+ ) {
423
+ return this.answer(this.lastDispatch, a, b, c, d);
424
+ }
425
+ return this.resolve(4, a, b, c, d);
426
+ }
427
+
428
+ /**
429
+ * The array form, for a caller that holds its arguments as one — a call written WIDER than
430
+ * any overload can be (`'42'.replace('2', '1', 1, false)` is five values, which is a row),
431
+ * the conformance and identity gates, and a host comparing a site cold against warm. It
432
+ * routes to the same entry points, so there is one dispatch path and not two.
433
+ */
434
+ call(values: readonly CelValue[]): CelValue {
435
+ switch (values.length) {
436
+ case 0:
437
+ return this.call0();
438
+ case 1:
439
+ return this.call1(values[0]!);
440
+ case 2:
441
+ return this.call2(values[0]!, values[1]!);
442
+ case 3:
443
+ return this.call3(values[0]!, values[1]!, values[2]!);
444
+ case 4:
445
+ return this.call4(values[0]!, values[1]!, values[2]!, values[3]!);
446
+ default:
447
+ return this.wide(values);
448
+ }
449
+ }
450
+
451
+ /**
452
+ * A call written with more values than the widest signature may declare. **The arity is
453
+ * the SOURCE's, not the dispatch key's** — anyone may write a call of any width — so this
454
+ * is reachable and answers the ordinary refusal, naming the types it was handed. Nothing
455
+ * resolves here, because a signature that wide is refused where it is registered; the
456
+ * resolution still runs, so one place decides what a call that resolves to nothing says.
457
+ */
458
+ private wide(values: readonly CelValue[]): CelValue {
459
+ const names: string[] = new Array<string>(values.length);
460
+ for (let at = 0; at < values.length; at += 1) {
461
+ const held = celTypeNameOf(values[at]);
462
+ if (held === undefined) {
463
+ const named = readHostValue(values[at], this.range);
464
+ if (isCelError(named)) return named;
465
+ return celError(
466
+ "no_matching_overload",
467
+ `${this.name} was handed a value of no CEL type`,
468
+ this.range,
469
+ );
470
+ }
471
+ names[at] = held;
472
+ }
473
+ return celError(
474
+ "no_matching_overload",
475
+ `no overload of ${JSON.stringify(this.name)} takes (${names.join(", ")})`,
476
+ this.range,
477
+ );
478
+ }
479
+
480
+ private resolve(
481
+ count: number,
482
+ a: CelValue | undefined,
483
+ b: CelValue | undefined,
484
+ c: CelValue | undefined,
485
+ d: CelValue | undefined,
486
+ ): CelValue {
487
+ const names: string[] = new Array<string>(count);
488
+ for (let at = 0; at < count; at += 1) {
489
+ const value = at === 0 ? a : at === 1 ? b : at === 2 ? c : d;
490
+ const held = celTypeNameOf(value);
491
+ if (held === undefined) {
492
+ // A value of no CEL type. A **thenable** is one, and a thenable NESTED inside a host
493
+ // value arrives here rather than through the activation read, because a member read
494
+ // hands back what the host put there — and `resources.x.status.y` is exactly that
495
+ // shape, so this is the door a host actually uses. `readHostValue` names it for what
496
+ // it is; anything else is the overload failure it was going to be. The cost is on the
497
+ // slow path only: dispatch was about to fail either way.
498
+ const named = readHostValue(value, this.range);
499
+ if (isCelError(named)) return named;
500
+ return celError(
501
+ "no_matching_overload",
502
+ `${this.name} was handed a value of no CEL type`,
503
+ this.range,
504
+ );
505
+ }
506
+ names[at] = held;
507
+ }
508
+ const key = names.join(",");
509
+ let dispatch = this.resolved.get(key);
510
+ if (dispatch === undefined) {
511
+ const types = names.map((unused, at) =>
512
+ runtimeType((at === 0 ? a : at === 1 ? b : at === 2 ? c : d) as CelValue, this.nominalArity),
513
+ );
514
+ const resolution = this.registry.resolve(
515
+ this.name,
516
+ this.form,
517
+ this.form === "receiver" ? types.slice(1) : types,
518
+ this.form === "receiver" ? types[0] : undefined,
519
+ );
520
+ dispatch = dispatchOf(this.name, resolution);
521
+ this.resolved.set(key, dispatch);
522
+ }
523
+ this.count = count;
524
+ this.t0 = names[0];
525
+ this.t1 = names[1];
526
+ this.t2 = names[2];
527
+ this.t3 = names[3];
528
+ this.lastDispatch = dispatch;
529
+ return this.answer(dispatch, a, b, c, d);
530
+ }
531
+
532
+ private answer(
533
+ dispatch: Dispatch | null,
534
+ a?: CelValue,
535
+ b?: CelValue,
536
+ c?: CelValue,
537
+ d?: CelValue,
538
+ ): CelValue {
539
+ if (!dispatch) {
540
+ const names = [this.t0, this.t1, this.t2, this.t3].slice(0, Math.max(this.count, 0));
541
+ return celError(
542
+ "no_matching_overload",
543
+ `no overload of ${JSON.stringify(this.name)} takes (${names.join(", ")})`,
544
+ this.range,
545
+ );
546
+ }
547
+ const value = dispatch.implementation(this.context, a, b, c, d);
548
+ return dispatch.foreign ? readHostValue(value, this.range) : value;
549
+ }
550
+ }
551
+
552
+ /** A call site over an environment, which both backends build one of per call. */
553
+ export function callSiteOf(
554
+ target: CompileTarget,
555
+ name: string,
556
+ form: CallForm,
557
+ range: SourceRange,
558
+ ): CallSite {
559
+ return new CallSite(name, form, range, target.registry, target.nominalArity);
560
+ }
561
+
562
+ /**
563
+ * The type of a value, for dispatch. A container's element type is `dyn`: reading it
564
+ * exactly would mean walking the data on every call, and the registry's loose pass
565
+ * resolves a parameterized overload against `dyn` anyway.
566
+ */
567
+ export function runtimeType(
568
+ value: CelValue,
569
+ nominalArity: (name: string) => number | undefined,
570
+ ): CelType {
571
+ const name = celTypeNameOf(value);
572
+ switch (name) {
573
+ case "int":
574
+ return INT;
575
+ case "uint":
576
+ return UINT;
577
+ case "double":
578
+ return DOUBLE;
579
+ case "bool":
580
+ return BOOL;
581
+ case "string":
582
+ return STRING;
583
+ case "bytes":
584
+ return BYTES;
585
+ case "null_type":
586
+ return NULL;
587
+ case "type":
588
+ return TYPE;
589
+ case "google.protobuf.Timestamp":
590
+ return TIMESTAMP;
591
+ case "google.protobuf.Duration":
592
+ return DURATION;
593
+ case "list":
594
+ return listOf(DYN);
595
+ case "map":
596
+ return mapOf(DYN, DYN);
597
+ case "optional":
598
+ return optionalOf(DYN);
599
+ default: {
600
+ const arity = nominalArity(name!) ?? 0;
601
+ return { kind: "nominal", name: name!, base: DYN, args: Array.from({ length: arity }, () => DYN) };
602
+ }
603
+ }
604
+ }