@telorun/cel 0.0.0-stage → 0.107.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +281 -2
- package/dist/activation.d.ts +27 -0
- package/dist/activation.d.ts.map +1 -0
- package/dist/activation.js +24 -0
- package/dist/backend-runtime.d.ts +146 -0
- package/dist/backend-runtime.d.ts.map +1 -0
- package/dist/backend-runtime.js +322 -0
- package/dist/bounded-cache.d.ts +21 -0
- package/dist/bounded-cache.d.ts.map +1 -0
- package/dist/bounded-cache.js +42 -0
- package/dist/catalog-runtime.d.ts +59 -0
- package/dist/catalog-runtime.d.ts.map +1 -0
- package/dist/catalog-runtime.js +785 -0
- package/dist/cel-expression.d.ts +32 -0
- package/dist/cel-expression.d.ts.map +1 -0
- package/dist/cel-expression.js +29 -0
- package/dist/cel-map-value.d.ts +34 -0
- package/dist/cel-map-value.d.ts.map +1 -0
- package/dist/cel-map-value.js +74 -0
- package/dist/cel-program.d.ts +44 -0
- package/dist/cel-program.d.ts.map +1 -0
- package/dist/cel-program.js +72 -0
- package/dist/cel-type.d.ts +131 -0
- package/dist/cel-type.d.ts.map +1 -0
- package/dist/cel-type.js +293 -0
- package/dist/cel-value.d.ts +159 -0
- package/dist/cel-value.d.ts.map +1 -0
- package/dist/cel-value.js +225 -0
- package/dist/check-diagnostic.d.ts +53 -0
- package/dist/check-diagnostic.d.ts.map +1 -0
- package/dist/check-diagnostic.js +66 -0
- package/dist/checker.d.ts +77 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +721 -0
- package/dist/closure-backend.d.ts +21 -0
- package/dist/closure-backend.d.ts.map +1 -0
- package/dist/closure-backend.js +436 -0
- package/dist/comprehension-bindings.d.ts +33 -0
- package/dist/comprehension-bindings.d.ts.map +1 -0
- package/dist/comprehension-bindings.js +50 -0
- package/dist/comprehension-runtime.d.ts +44 -0
- package/dist/comprehension-runtime.d.ts.map +1 -0
- package/dist/comprehension-runtime.js +137 -0
- package/dist/declared-chain.d.ts +35 -0
- package/dist/declared-chain.d.ts.map +1 -0
- package/dist/declared-chain.js +36 -0
- package/dist/duration-value.d.ts +59 -0
- package/dist/duration-value.d.ts.map +1 -0
- package/dist/duration-value.js +135 -0
- package/dist/emitted-module.d.ts +207 -0
- package/dist/emitted-module.d.ts.map +1 -0
- package/dist/emitted-module.js +359 -0
- package/dist/engine-version.d.ts +3 -0
- package/dist/engine-version.d.ts.map +1 -0
- package/dist/engine-version.js +8 -0
- package/dist/environment-digest.d.ts +44 -0
- package/dist/environment-digest.d.ts.map +1 -0
- package/dist/environment-digest.js +98 -0
- package/dist/environment.d.ts +283 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +459 -0
- package/dist/function-catalog.d.ts +66 -0
- package/dist/function-catalog.d.ts.map +1 -0
- package/dist/function-catalog.js +77 -0
- package/dist/function-registry.d.ts +78 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +189 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/integer-arithmetic.d.ts +27 -0
- package/dist/integer-arithmetic.d.ts.map +1 -0
- package/dist/integer-arithmetic.js +58 -0
- package/dist/js-emitter.d.ts +132 -0
- package/dist/js-emitter.d.ts.map +1 -0
- package/dist/js-emitter.js +562 -0
- package/dist/json-schema-type.d.ts +182 -0
- package/dist/json-schema-type.d.ts.map +1 -0
- package/dist/json-schema-type.js +487 -0
- package/dist/json-text-scan.d.ts +28 -0
- package/dist/json-text-scan.d.ts.map +1 -0
- package/dist/json-text-scan.js +159 -0
- package/dist/lexer.d.ts +103 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +458 -0
- package/dist/macro-check.d.ts +33 -0
- package/dist/macro-check.d.ts.map +1 -0
- package/dist/macro-check.js +162 -0
- package/dist/macro-shape.d.ts +24 -0
- package/dist/macro-shape.d.ts.map +1 -0
- package/dist/macro-shape.js +55 -0
- package/dist/member-read.d.ts +52 -0
- package/dist/member-read.d.ts.map +1 -0
- package/dist/member-read.js +125 -0
- package/dist/namespace-resolution.d.ts +35 -0
- package/dist/namespace-resolution.d.ts.map +1 -0
- package/dist/namespace-resolution.js +160 -0
- package/dist/nominal-type.d.ts +63 -0
- package/dist/nominal-type.d.ts.map +1 -0
- package/dist/nominal-type.js +98 -0
- package/dist/nullable-access.d.ts +38 -0
- package/dist/nullable-access.d.ts.map +1 -0
- package/dist/nullable-access.js +93 -0
- package/dist/parse-limits.d.ts +26 -0
- package/dist/parse-limits.d.ts.map +1 -0
- package/dist/parse-limits.js +21 -0
- package/dist/parser.d.ts +48 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +503 -0
- package/dist/qualified-calls.d.ts +22 -0
- package/dist/qualified-calls.d.ts.map +1 -0
- package/dist/qualified-calls.js +27 -0
- package/dist/regular-expression.d.ts +48 -0
- package/dist/regular-expression.d.ts.map +1 -0
- package/dist/regular-expression.js +77 -0
- package/dist/reserved-words.d.ts +52 -0
- package/dist/reserved-words.d.ts.map +1 -0
- package/dist/reserved-words.js +77 -0
- package/dist/resolved-call.d.ts +33 -0
- package/dist/resolved-call.d.ts.map +1 -0
- package/dist/resolved-call.js +15 -0
- package/dist/root-references.d.ts +23 -0
- package/dist/root-references.d.ts.map +1 -0
- package/dist/root-references.js +120 -0
- package/dist/runtime-library.d.ts +56 -0
- package/dist/runtime-library.d.ts.map +1 -0
- package/dist/runtime-library.js +545 -0
- package/dist/serializer.d.ts +24 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +240 -0
- package/dist/sha256.d.ts +20 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +103 -0
- package/dist/signature.d.ts +72 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +61 -0
- package/dist/signatures/function-catalog.json +788 -0
- package/dist/signatures/standard-library.json +229 -0
- package/dist/standard-library.d.ts +41 -0
- package/dist/standard-library.d.ts.map +1 -0
- package/dist/standard-library.js +85 -0
- package/dist/syntax-diagnostic.d.ts +61 -0
- package/dist/syntax-diagnostic.d.ts.map +1 -0
- package/dist/syntax-diagnostic.js +28 -0
- package/dist/syntax-tree.d.ts +160 -0
- package/dist/syntax-tree.d.ts.map +1 -0
- package/dist/syntax-tree.js +58 -0
- package/dist/timestamp-value.d.ts +53 -0
- package/dist/timestamp-value.d.ts.map +1 -0
- package/dist/timestamp-value.js +223 -0
- package/dist/tree-equality.d.ts +15 -0
- package/dist/tree-equality.d.ts.map +1 -0
- package/dist/tree-equality.js +105 -0
- package/dist/type-expression.d.ts +26 -0
- package/dist/type-expression.d.ts.map +1 -0
- package/dist/type-expression.js +134 -0
- package/dist/value-equality.d.ts +38 -0
- package/dist/value-equality.d.ts.map +1 -0
- package/dist/value-equality.js +196 -0
- package/dist/value-text.d.ts +25 -0
- package/dist/value-text.d.ts.map +1 -0
- package/dist/value-text.js +44 -0
- package/dist/zoned-calendar.d.ts +61 -0
- package/dist/zoned-calendar.d.ts.map +1 -0
- package/dist/zoned-calendar.js +143 -0
- package/package.json +56 -3
- package/src/activation.ts +32 -0
- package/src/backend-runtime.ts +454 -0
- package/src/bounded-cache.ts +45 -0
- package/src/catalog-runtime.ts +921 -0
- package/src/cel-expression.ts +53 -0
- package/src/cel-map-value.ts +86 -0
- package/src/cel-program.ts +103 -0
- package/src/cel-type.ts +359 -0
- package/src/cel-value.ts +353 -0
- package/src/check-diagnostic.ts +102 -0
- package/src/checker.ts +932 -0
- package/src/closure-backend.ts +502 -0
- package/src/comprehension-bindings.ts +66 -0
- package/src/comprehension-runtime.ts +157 -0
- package/src/declared-chain.ts +45 -0
- package/src/duration-value.ts +145 -0
- package/src/emitted-module.ts +494 -0
- package/src/engine-version.ts +9 -0
- package/src/environment-digest.ts +111 -0
- package/src/environment.ts +740 -0
- package/src/function-catalog.ts +140 -0
- package/src/function-registry.ts +229 -0
- package/src/index.ts +386 -0
- package/src/integer-arithmetic.ts +64 -0
- package/src/js-emitter.ts +713 -0
- package/src/json-schema-type.ts +664 -0
- package/src/json-text-scan.ts +163 -0
- package/src/lexer.ts +562 -0
- package/src/macro-check.ts +191 -0
- package/src/macro-shape.ts +66 -0
- package/src/member-read.ts +126 -0
- package/src/namespace-resolution.ts +167 -0
- package/src/nominal-type.ts +149 -0
- package/src/nullable-access.ts +95 -0
- package/src/parse-limits.ts +36 -0
- package/src/parser.ts +554 -0
- package/src/qualified-calls.ts +39 -0
- package/src/regular-expression.ts +101 -0
- package/src/reserved-words.ts +94 -0
- package/src/resolved-call.ts +34 -0
- package/src/root-references.ts +126 -0
- package/src/runtime-library.ts +615 -0
- package/src/serializer.ts +246 -0
- package/src/sha256.ts +112 -0
- package/src/signature.ts +127 -0
- package/src/signatures/function-catalog.json +788 -0
- package/src/signatures/standard-library.json +235 -0
- package/src/standard-library.ts +149 -0
- package/src/syntax-diagnostic.ts +72 -0
- package/src/syntax-tree.ts +229 -0
- package/src/timestamp-value.ts +294 -0
- package/src/tree-equality.ts +130 -0
- package/src/type-expression.ts +160 -0
- package/src/value-equality.ts +201 -0
- package/src/value-text.ts +45 -0
- package/src/zoned-calendar.ts +178 -0
|
@@ -0,0 +1,454 @@
|
|
|
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
|
+
celError,
|
|
43
|
+
celNone,
|
|
44
|
+
celSome,
|
|
45
|
+
celTypeNameOf,
|
|
46
|
+
isCelError,
|
|
47
|
+
isCelOptional,
|
|
48
|
+
} from "./cel-value.js";
|
|
49
|
+
import type { FunctionRegistry, Resolution, ResolutionFailure } from "./function-registry.js";
|
|
50
|
+
import { celHas, celLookup, celRead, lookupError, MISSING, OUT_OF_RANGE } from "./member-read.js";
|
|
51
|
+
import type { CelCallContext, CelImplementation } from "./runtime-library.js";
|
|
52
|
+
import { implementationOf } from "./runtime-library.js";
|
|
53
|
+
import type { CallForm } from "./signature.js";
|
|
54
|
+
import type { CelNode, CelSelectNode, SourceRange } from "./syntax-tree.js";
|
|
55
|
+
|
|
56
|
+
/** A compile-time refusal: a tree neither backend can compile at all. */
|
|
57
|
+
export class CelCompileError extends Error {
|
|
58
|
+
constructor(message: string) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "CelCompileError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** How many distinct argument-type combinations one call site remembers. */
|
|
65
|
+
export const CALL_SITE_CACHE_CAPACITY = 16;
|
|
66
|
+
|
|
67
|
+
/** What a namespaced call is dispatched through, when something bound it. */
|
|
68
|
+
export type NamespaceDispatch = (namespace: string, name: string) => CelImplementation | undefined;
|
|
69
|
+
|
|
70
|
+
export interface EvaluationFrame {
|
|
71
|
+
readonly activation: CelActivation;
|
|
72
|
+
readonly slots: CelValue[];
|
|
73
|
+
/** Absent rather than optional: one frame shape, however the evaluation was started. */
|
|
74
|
+
readonly namespaceFunction: NamespaceDispatch | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One evaluation of one expression. The closure backend builds these by composition and
|
|
79
|
+
* the emitter's module returns them already built, so a program is indifferent to which
|
|
80
|
+
* backend produced it.
|
|
81
|
+
*/
|
|
82
|
+
export type CelStep = (frame: EvaluationFrame) => CelValue;
|
|
83
|
+
|
|
84
|
+
/** What a backend needs of the environment it compiles against. */
|
|
85
|
+
export interface CompileTarget {
|
|
86
|
+
readonly registry: FunctionRegistry;
|
|
87
|
+
/** The library's own names — the type values and `google`. */
|
|
88
|
+
readonly constants: ReadonlyMap<string, CelValue>;
|
|
89
|
+
/** How many type arguments a host's named type takes, for dispatch on one of its values. */
|
|
90
|
+
readonly nominalArity: (name: string) => number | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Whether a host DECLARED this name — including a dotted one. It decides where a chain
|
|
93
|
+
* splits, at compile time, through the same function the checker uses.
|
|
94
|
+
*/
|
|
95
|
+
readonly declares: (name: string) => boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- host values -----------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A host value entering the engine — from an activation, from an implementation the host
|
|
102
|
+
* registered, or out of a host value a read reached into. A thenable is refused here rather
|
|
103
|
+
* than carried: an awaiting expression is an invocation in disguise, invisible to a journal
|
|
104
|
+
* and absent from a trace. The refusal itself lives in `cel-value.ts`, so every door answers
|
|
105
|
+
* with the same code and the same wording.
|
|
106
|
+
*/
|
|
107
|
+
export function readHostValue(value: unknown, range: SourceRange): CelValue {
|
|
108
|
+
return asyncValueRefused(value, range) ?? (value as CelValue);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- names -----------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
/** A bare name: the activation's own entry, then the library's constants. */
|
|
114
|
+
export function readName(
|
|
115
|
+
activation: CelActivation,
|
|
116
|
+
constants: ReadonlyMap<string, CelValue>,
|
|
117
|
+
name: string,
|
|
118
|
+
range: SourceRange,
|
|
119
|
+
): CelValue {
|
|
120
|
+
if (activationHolds(activation, name)) return readHostValue(activation[name], range);
|
|
121
|
+
const held = constants.get(name);
|
|
122
|
+
if (held !== undefined) return held;
|
|
123
|
+
return celError("no_such_variable", `no such variable: ${name}`, range);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One name read from the activation (or the library's constants), then its members. This
|
|
128
|
+
* is a chain whose split is already decided — at compile time, over the names the host
|
|
129
|
+
* declared (`declared-chain.ts`) — so evaluating it is one lookup plus member reads.
|
|
130
|
+
*/
|
|
131
|
+
export function readNameChain(
|
|
132
|
+
activation: CelActivation,
|
|
133
|
+
constants: ReadonlyMap<string, CelValue>,
|
|
134
|
+
name: string,
|
|
135
|
+
rest: readonly string[],
|
|
136
|
+
range: SourceRange,
|
|
137
|
+
): CelValue {
|
|
138
|
+
let held: CelValue;
|
|
139
|
+
if (activationHolds(activation, name)) held = readHostValue(activation[name], range);
|
|
140
|
+
else {
|
|
141
|
+
const constant = constants.get(name);
|
|
142
|
+
if (constant === undefined) return celError("no_such_variable", `no such variable: ${name}`, range);
|
|
143
|
+
held = constant;
|
|
144
|
+
}
|
|
145
|
+
for (const field of rest) {
|
|
146
|
+
if (isCelError(held)) return held;
|
|
147
|
+
held = readThrough(held, field, false, range);
|
|
148
|
+
}
|
|
149
|
+
return held;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One prefix of a dotted chain, and the segments left to read off its value. */
|
|
153
|
+
export interface ChainCandidate {
|
|
154
|
+
readonly name: string;
|
|
155
|
+
readonly rest: readonly string[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Every prefix of a dotted chain, longest first, with the segments left to read. */
|
|
159
|
+
export function prefixCandidates(segments: readonly string[]): readonly ChainCandidate[] {
|
|
160
|
+
const candidates: ChainCandidate[] = [];
|
|
161
|
+
for (let length = segments.length; length >= 1; length -= 1) {
|
|
162
|
+
candidates.push({ name: segments.slice(0, length).join("."), rest: segments.slice(length) });
|
|
163
|
+
}
|
|
164
|
+
return candidates;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A chain **no** prefix of which the host declared: the activation is searched, longest
|
|
169
|
+
* prefix first. Every conformance row that binds a dotted key reads this way, and the
|
|
170
|
+
* checker has no opinion about such a chain either.
|
|
171
|
+
*/
|
|
172
|
+
export function searchNameChain(
|
|
173
|
+
activation: CelActivation,
|
|
174
|
+
constants: ReadonlyMap<string, CelValue>,
|
|
175
|
+
candidates: readonly ChainCandidate[],
|
|
176
|
+
written: string,
|
|
177
|
+
range: SourceRange,
|
|
178
|
+
): CelValue {
|
|
179
|
+
for (let at = 0; at < candidates.length; at += 1) {
|
|
180
|
+
const candidate = candidates[at]!;
|
|
181
|
+
if (!activationHolds(activation, candidate.name) && !constants.has(candidate.name)) continue;
|
|
182
|
+
return readNameChain(activation, constants, candidate.name, candidate.rest, range);
|
|
183
|
+
}
|
|
184
|
+
return celError("no_such_variable", `no such variable: ${written}`, range);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The dotted chain a select spells, when every step is a plain named member of a **free**
|
|
189
|
+
* name. A name a macro bound is a value, so a chain rooted at one is an ordinary member
|
|
190
|
+
* read — the same rule the checker applies, and the same answer both backends compile.
|
|
191
|
+
*/
|
|
192
|
+
export function plainMemberChain(
|
|
193
|
+
node: CelSelectNode,
|
|
194
|
+
bound: (name: string) => boolean,
|
|
195
|
+
): readonly string[] | undefined {
|
|
196
|
+
const segments: string[] = [];
|
|
197
|
+
let at: CelNode = node;
|
|
198
|
+
while (at.kind === "select") {
|
|
199
|
+
if (at.optional || at.field === "") return undefined;
|
|
200
|
+
segments.unshift(at.field);
|
|
201
|
+
at = at.operand;
|
|
202
|
+
}
|
|
203
|
+
if (at.kind !== "ident") return undefined;
|
|
204
|
+
if (!at.absolute && bound(at.name)) return undefined;
|
|
205
|
+
segments.unshift(at.name);
|
|
206
|
+
return segments;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- member reads ----------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A member read in every form. Reading **through an optional** answers an optional
|
|
213
|
+
* whichever form the read is written in, which is what lets a chain over a value that
|
|
214
|
+
* may be absent stay one expression: an absent one propagates as absent, a key the held
|
|
215
|
+
* value does not have is absent too, and a held value that holds no members at all is
|
|
216
|
+
* still the mistake it would be outside an optional.
|
|
217
|
+
*/
|
|
218
|
+
export function readThrough(
|
|
219
|
+
container: CelValue,
|
|
220
|
+
key: CelValue,
|
|
221
|
+
optionalForm: boolean,
|
|
222
|
+
range: SourceRange,
|
|
223
|
+
): CelValue {
|
|
224
|
+
if (isCelOptional(container)) {
|
|
225
|
+
if (!container.present) return celNone();
|
|
226
|
+
return optionalRead(container.held as CelValue, key, range);
|
|
227
|
+
}
|
|
228
|
+
if (optionalForm) return optionalRead(container, key, range);
|
|
229
|
+
// **A member read is a door a host value comes through**, and the value it answers is
|
|
230
|
+
// whatever the host put inside its own object: `readHostValue` is what refuses a thenable
|
|
231
|
+
// there, rather than the aggregate, the operator or the exit that happens to see it next.
|
|
232
|
+
return readHostValue(celRead(container, key, range), range);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function optionalRead(container: CelValue, key: CelValue, range: SourceRange): CelValue {
|
|
236
|
+
const found = celLookup(container, key);
|
|
237
|
+
if (typeof found !== "symbol") {
|
|
238
|
+
const held = readHostValue(found, range);
|
|
239
|
+
// A value that must be awaited is refused rather than carried as a present optional.
|
|
240
|
+
return isCelError(held) ? held : celSome(held);
|
|
241
|
+
}
|
|
242
|
+
if (found === MISSING || found === OUT_OF_RANGE) return celNone();
|
|
243
|
+
return lookupError(found, key, range);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* `has(a.b)` — presence, which a missing key answers `false` for rather than erroring.
|
|
248
|
+
* An absent optional has no members, and a present one is asked about what it holds.
|
|
249
|
+
*/
|
|
250
|
+
export function hasMember(container: CelValue, field: CelValue, range: SourceRange): CelValue {
|
|
251
|
+
if (isCelOptional(container)) {
|
|
252
|
+
return container.present ? celHas(container.held as CelValue, field, range) : false;
|
|
253
|
+
}
|
|
254
|
+
return celHas(container, field, range);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- operands --------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
/** A bool operand, or the error it is — a non-bool is a mistake of its own. */
|
|
260
|
+
export function boolOperand(value: CelValue, range: SourceRange): boolean | CelError {
|
|
261
|
+
if (typeof value === "boolean") return value;
|
|
262
|
+
if (isCelError(value)) return value;
|
|
263
|
+
return celError("no_matching_overload", "this value is not a bool", range);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** What an optional entry of an aggregate contributes, or the error it is not one. */
|
|
267
|
+
export function optionalEntry(value: CelValue, range: SourceRange): CelOptional | CelError {
|
|
268
|
+
if (isCelOptional(value)) return value;
|
|
269
|
+
return celError("no_matching_overload", "an entry written with '?' holds an optional", range);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- one call site ---------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* What a call site resolved to: the behaviour, and whether it came from the HOST. The
|
|
276
|
+
* engine's own implementations are typed and cannot answer a thenable, so only a host's
|
|
277
|
+
* answer is checked for one — which keeps the check off the path every operator takes.
|
|
278
|
+
*/
|
|
279
|
+
interface Dispatch {
|
|
280
|
+
readonly implementation: CelImplementation;
|
|
281
|
+
readonly foreign: boolean;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function dispatchOf(name: string, resolution: Resolution | ResolutionFailure): Dispatch | null {
|
|
285
|
+
if ("resolved" in resolution) {
|
|
286
|
+
const host = resolution.resolved.metadata.implementation;
|
|
287
|
+
if (host) return { implementation: host, foreign: true };
|
|
288
|
+
const own = implementationOf(resolution.resolved.signature);
|
|
289
|
+
return own ? { implementation: own, foreign: false } : null;
|
|
290
|
+
}
|
|
291
|
+
// **Equality is universal at runtime**, over every pair of types: the checker refuses
|
|
292
|
+
// `1 == 1u` deliberately, but `dyn(1) == 1u` reaches evaluation and cel-spec answers
|
|
293
|
+
// `true` there. A host's own registration still wins, because it resolved first.
|
|
294
|
+
const equality = equalityFallback(name);
|
|
295
|
+
return equality ? { implementation: equality, foreign: false } : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The universal equality, for a pair of types no registration names. */
|
|
299
|
+
function equalityFallback(name: string): CelImplementation | null {
|
|
300
|
+
if (name !== "==" && name !== "!=") return null;
|
|
301
|
+
return implementationOf({ name, form: "global", parameters: [], returns: DYN }) ?? null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* One call site, and the overloads it has resolved.
|
|
306
|
+
*
|
|
307
|
+
* **Overloads are resolved on the values' own types**, per site: `dyn(1.0) == 1` checks and
|
|
308
|
+
* must then answer across the numeric types, so the statically resolved signature is not
|
|
309
|
+
* enough. A site is almost always monomorphic, so the last resolution is held beside a
|
|
310
|
+
* bounded cache and reached by comparing the type names themselves — building a cache key
|
|
311
|
+
* per call is the allocation that costs most on the hottest path there is. A container's
|
|
312
|
+
* element type is read as `dyn` rather than walked, so dispatch does not get more expensive
|
|
313
|
+
* as the data gets larger.
|
|
314
|
+
*
|
|
315
|
+
* The arguments arrive **already evaluated and already free of errors**: carrying an
|
|
316
|
+
* error-valued operand out is the caller's, because only the caller knows how far it got.
|
|
317
|
+
*/
|
|
318
|
+
export class CallSite {
|
|
319
|
+
private readonly resolved = new BoundedCache<string, Dispatch | null>(CALL_SITE_CACHE_CAPACITY);
|
|
320
|
+
private readonly context: CelCallContext;
|
|
321
|
+
private lastNames: readonly string[] | undefined;
|
|
322
|
+
private lastDispatch: Dispatch | null = null;
|
|
323
|
+
|
|
324
|
+
constructor(
|
|
325
|
+
private readonly name: string,
|
|
326
|
+
private readonly form: CallForm,
|
|
327
|
+
private readonly range: SourceRange,
|
|
328
|
+
private readonly registry: FunctionRegistry,
|
|
329
|
+
private readonly nominalArity: (name: string) => number | undefined,
|
|
330
|
+
) {
|
|
331
|
+
this.context = { range };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
call(values: readonly CelValue[]): CelValue {
|
|
335
|
+
const count = values.length;
|
|
336
|
+
const last = this.lastNames;
|
|
337
|
+
if (last !== undefined && last.length === count) {
|
|
338
|
+
let same = true;
|
|
339
|
+
for (let at = 0; at < count; at += 1) {
|
|
340
|
+
if (celTypeNameOf(values[at]!) !== last[at]) {
|
|
341
|
+
same = false;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (same) return this.answer(this.lastDispatch, values, last);
|
|
346
|
+
}
|
|
347
|
+
const names: string[] = new Array<string>(count);
|
|
348
|
+
for (let at = 0; at < count; at += 1) {
|
|
349
|
+
const held = celTypeNameOf(values[at]!);
|
|
350
|
+
if (held === undefined) {
|
|
351
|
+
// A value of no CEL type. A **thenable** is one, and a thenable NESTED inside a host
|
|
352
|
+
// value arrives here rather than through the activation read, because a member read
|
|
353
|
+
// hands back what the host put there — and `resources.x.status.y` is exactly that
|
|
354
|
+
// shape, so this is the door a host actually uses. `readHostValue` names it for what
|
|
355
|
+
// it is; anything else is the overload failure it was going to be. The cost is on the
|
|
356
|
+
// slow path only: dispatch was about to fail either way.
|
|
357
|
+
const named = readHostValue(values[at], this.range);
|
|
358
|
+
if (isCelError(named)) return named;
|
|
359
|
+
return celError(
|
|
360
|
+
"no_matching_overload",
|
|
361
|
+
`${this.name} was handed a value of no CEL type`,
|
|
362
|
+
this.range,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
names[at] = held;
|
|
366
|
+
}
|
|
367
|
+
const key = names.join(",");
|
|
368
|
+
let dispatch = this.resolved.get(key);
|
|
369
|
+
if (dispatch === undefined) {
|
|
370
|
+
const types = values.map((value) => runtimeType(value, this.nominalArity));
|
|
371
|
+
const resolution = this.registry.resolve(
|
|
372
|
+
this.name,
|
|
373
|
+
this.form,
|
|
374
|
+
this.form === "receiver" ? types.slice(1) : types,
|
|
375
|
+
this.form === "receiver" ? types[0] : undefined,
|
|
376
|
+
);
|
|
377
|
+
dispatch = dispatchOf(this.name, resolution);
|
|
378
|
+
this.resolved.set(key, dispatch);
|
|
379
|
+
}
|
|
380
|
+
this.lastNames = names;
|
|
381
|
+
this.lastDispatch = dispatch;
|
|
382
|
+
return this.answer(dispatch, values, names);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private answer(
|
|
386
|
+
dispatch: Dispatch | null,
|
|
387
|
+
values: readonly CelValue[],
|
|
388
|
+
names: readonly string[],
|
|
389
|
+
): CelValue {
|
|
390
|
+
if (!dispatch) {
|
|
391
|
+
return celError(
|
|
392
|
+
"no_matching_overload",
|
|
393
|
+
`no overload of ${JSON.stringify(this.name)} takes (${names.join(", ")})`,
|
|
394
|
+
this.range,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const value = dispatch.implementation(values, this.context);
|
|
398
|
+
return dispatch.foreign ? readHostValue(value, this.range) : value;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** A call site over an environment, which both backends build one of per call. */
|
|
403
|
+
export function callSiteOf(
|
|
404
|
+
target: CompileTarget,
|
|
405
|
+
name: string,
|
|
406
|
+
form: CallForm,
|
|
407
|
+
range: SourceRange,
|
|
408
|
+
): CallSite {
|
|
409
|
+
return new CallSite(name, form, range, target.registry, target.nominalArity);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The type of a value, for dispatch. A container's element type is `dyn`: reading it
|
|
414
|
+
* exactly would mean walking the data on every call, and the registry's loose pass
|
|
415
|
+
* resolves a parameterized overload against `dyn` anyway.
|
|
416
|
+
*/
|
|
417
|
+
export function runtimeType(
|
|
418
|
+
value: CelValue,
|
|
419
|
+
nominalArity: (name: string) => number | undefined,
|
|
420
|
+
): CelType {
|
|
421
|
+
const name = celTypeNameOf(value);
|
|
422
|
+
switch (name) {
|
|
423
|
+
case "int":
|
|
424
|
+
return INT;
|
|
425
|
+
case "uint":
|
|
426
|
+
return UINT;
|
|
427
|
+
case "double":
|
|
428
|
+
return DOUBLE;
|
|
429
|
+
case "bool":
|
|
430
|
+
return BOOL;
|
|
431
|
+
case "string":
|
|
432
|
+
return STRING;
|
|
433
|
+
case "bytes":
|
|
434
|
+
return BYTES;
|
|
435
|
+
case "null_type":
|
|
436
|
+
return NULL;
|
|
437
|
+
case "type":
|
|
438
|
+
return TYPE;
|
|
439
|
+
case "google.protobuf.Timestamp":
|
|
440
|
+
return TIMESTAMP;
|
|
441
|
+
case "google.protobuf.Duration":
|
|
442
|
+
return DURATION;
|
|
443
|
+
case "list":
|
|
444
|
+
return listOf(DYN);
|
|
445
|
+
case "map":
|
|
446
|
+
return mapOf(DYN, DYN);
|
|
447
|
+
case "optional":
|
|
448
|
+
return optionalOf(DYN);
|
|
449
|
+
default: {
|
|
450
|
+
const arity = nominalArity(name!) ?? 0;
|
|
451
|
+
return { kind: "nominal", name: name!, base: DYN, args: Array.from({ length: arity }, () => DYN) };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded least-recently-used cache.
|
|
3
|
+
*
|
|
4
|
+
* Every in-process cache in this engine is bounded with a declared capacity: a compiled
|
|
5
|
+
* expression, a compiled pattern and a call site's resolved overloads are all keyed on
|
|
6
|
+
* text a request can influence, so an unbounded cache is a memory leak with an author's
|
|
7
|
+
* input as its key. Eviction is least-recently-used, which a `Map` gives for free: its
|
|
8
|
+
* iteration order is insertion order, so re-inserting a hit moves it to the end and the
|
|
9
|
+
* first key is always the oldest untouched one.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class BoundedCache<Key, Value> {
|
|
13
|
+
private readonly held = new Map<Key, Value>();
|
|
14
|
+
|
|
15
|
+
constructor(readonly capacity: number) {
|
|
16
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
17
|
+
throw new RangeError("a bounded cache holds at least one entry");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get(key: Key): Value | undefined {
|
|
22
|
+
if (!this.held.has(key)) return undefined;
|
|
23
|
+
const value = this.held.get(key)!;
|
|
24
|
+
this.held.delete(key);
|
|
25
|
+
this.held.set(key, value);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(key: Key, value: Value): void {
|
|
30
|
+
this.held.delete(key);
|
|
31
|
+
this.held.set(key, value);
|
|
32
|
+
if (this.held.size > this.capacity) {
|
|
33
|
+
this.held.delete(this.held.keys().next().value as Key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Forgets everything — what a registration does to an environment's compiled programs. */
|
|
38
|
+
clear(): void {
|
|
39
|
+
this.held.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get size(): number {
|
|
43
|
+
return this.held.size;
|
|
44
|
+
}
|
|
45
|
+
}
|