@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.
- 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 +184 -0
- package/dist/backend-runtime.d.ts.map +1 -0
- package/dist/backend-runtime.js +425 -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 +786 -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 +47 -0
- package/dist/cel-map-value.d.ts.map +1 -0
- package/dist/cel-map-value.js +85 -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 +166 -0
- package/dist/cel-value.d.ts.map +1 -0
- package/dist/cel-value.js +225 -0
- package/dist/check-diagnostic.d.ts +54 -0
- package/dist/check-diagnostic.d.ts.map +1 -0
- package/dist/check-diagnostic.js +66 -0
- package/dist/checker.d.ts +86 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +806 -0
- package/dist/closure-backend.d.ts +21 -0
- package/dist/closure-backend.d.ts.map +1 -0
- package/dist/closure-backend.js +487 -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 +70 -0
- package/dist/duration-value.d.ts.map +1 -0
- package/dist/duration-value.js +149 -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 +291 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +474 -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 +118 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +292 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -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 +133 -0
- package/dist/js-emitter.d.ts.map +1 -0
- package/dist/js-emitter.js +568 -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 +41 -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 +89 -0
- package/dist/member-read.d.ts.map +1 -0
- package/dist/member-read.js +166 -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 +46 -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 +67 -0
- package/dist/runtime-library.d.ts.map +1 -0
- package/dist/runtime-library.js +554 -0
- package/dist/serializer.d.ts +24 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +256 -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 +62 -0
- package/dist/timestamp-value.d.ts.map +1 -0
- package/dist/timestamp-value.js +238 -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 +42 -0
- package/dist/type-expression.d.ts.map +1 -0
- package/dist/type-expression.js +154 -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 +604 -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 +97 -0
- package/src/cel-program.ts +103 -0
- package/src/cel-type.ts +359 -0
- package/src/cel-value.ts +361 -0
- package/src/check-diagnostic.ts +104 -0
- package/src/checker.ts +1045 -0
- package/src/closure-backend.ts +547 -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 +160 -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 +761 -0
- package/src/function-catalog.ts +140 -0
- package/src/function-registry.ts +341 -0
- package/src/index.ts +407 -0
- package/src/integer-arithmetic.ts +64 -0
- package/src/js-emitter.ts +721 -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 +197 -0
- package/src/macro-shape.ts +66 -0
- package/src/member-read.ts +167 -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 +47 -0
- package/src/root-references.ts +126 -0
- package/src/runtime-library.ts +639 -0
- package/src/serializer.ts +262 -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 +310 -0
- package/src/tree-equality.ts +130 -0
- package/src/type-expression.ts +182 -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,425 @@
|
|
|
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
|
+
import { activationHolds } from "./activation.js";
|
|
19
|
+
import { BoundedCache } from "./bounded-cache.js";
|
|
20
|
+
import { BOOL, BYTES, DOUBLE, DURATION, DYN, INT, listOf, mapOf, NULL, optionalOf, STRING, TIMESTAMP, TYPE, UINT, } from "./cel-type.js";
|
|
21
|
+
import { asyncValueRefused, CEL_VALUE_TYPE, celError, celNone, celSome, celTypeNameOf, isCelError, isCelOptional, } from "./cel-value.js";
|
|
22
|
+
import { celHas, celLookup, celRead, lookupAbsence, lookupError } from "./member-read.js";
|
|
23
|
+
import { implementationOf } from "./runtime-library.js";
|
|
24
|
+
/** A compile-time refusal: a tree neither backend can compile at all. */
|
|
25
|
+
export class CelCompileError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "CelCompileError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** How many distinct argument-type combinations one call site remembers. */
|
|
32
|
+
export const CALL_SITE_CACHE_CAPACITY = 16;
|
|
33
|
+
// --- host values -----------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* A host value entering the engine — from an activation, from an implementation the host
|
|
36
|
+
* registered, or out of a host value a read reached into. A thenable is refused here rather
|
|
37
|
+
* than carried: an awaiting expression is an invocation in disguise, invisible to a journal
|
|
38
|
+
* and absent from a trace. The refusal itself lives in `cel-value.ts`, so every door answers
|
|
39
|
+
* with the same code and the same wording.
|
|
40
|
+
*/
|
|
41
|
+
export function readHostValue(value, range) {
|
|
42
|
+
return asyncValueRefused(value, range) ?? value;
|
|
43
|
+
}
|
|
44
|
+
// --- names -----------------------------------------------------------------
|
|
45
|
+
/** A bare name: the activation's own entry, then the library's constants. */
|
|
46
|
+
export function readName(activation, constants, name, range) {
|
|
47
|
+
if (activationHolds(activation, name))
|
|
48
|
+
return readHostValue(activation[name], range);
|
|
49
|
+
const held = constants.get(name);
|
|
50
|
+
if (held !== undefined)
|
|
51
|
+
return held;
|
|
52
|
+
return celError("no_such_variable", `no such variable: ${name}`, range);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One name read from the activation (or the library's constants), then its members. This
|
|
56
|
+
* is a chain whose split is already decided — at compile time, over the names the host
|
|
57
|
+
* declared (`declared-chain.ts`) — so evaluating it is one lookup plus member reads.
|
|
58
|
+
*/
|
|
59
|
+
export function readNameChain(activation, constants, name, rest, range) {
|
|
60
|
+
let held;
|
|
61
|
+
if (activationHolds(activation, name))
|
|
62
|
+
held = readHostValue(activation[name], range);
|
|
63
|
+
else {
|
|
64
|
+
const constant = constants.get(name);
|
|
65
|
+
if (constant === undefined)
|
|
66
|
+
return celError("no_such_variable", `no such variable: ${name}`, range);
|
|
67
|
+
held = constant;
|
|
68
|
+
}
|
|
69
|
+
for (const field of rest) {
|
|
70
|
+
if (isCelError(held))
|
|
71
|
+
return held;
|
|
72
|
+
held = readThrough(held, field, false, range);
|
|
73
|
+
}
|
|
74
|
+
return held;
|
|
75
|
+
}
|
|
76
|
+
/** Every prefix of a dotted chain, longest first, with the segments left to read. */
|
|
77
|
+
export function prefixCandidates(segments) {
|
|
78
|
+
const candidates = [];
|
|
79
|
+
for (let length = segments.length; length >= 1; length -= 1) {
|
|
80
|
+
candidates.push({ name: segments.slice(0, length).join("."), rest: segments.slice(length) });
|
|
81
|
+
}
|
|
82
|
+
return candidates;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A chain **no** prefix of which the host declared: the activation is searched, longest
|
|
86
|
+
* prefix first. Every conformance row that binds a dotted key reads this way, and the
|
|
87
|
+
* checker has no opinion about such a chain either.
|
|
88
|
+
*/
|
|
89
|
+
export function searchNameChain(activation, constants, candidates, written, range) {
|
|
90
|
+
for (let at = 0; at < candidates.length; at += 1) {
|
|
91
|
+
const candidate = candidates[at];
|
|
92
|
+
if (!activationHolds(activation, candidate.name) && !constants.has(candidate.name))
|
|
93
|
+
continue;
|
|
94
|
+
return readNameChain(activation, constants, candidate.name, candidate.rest, range);
|
|
95
|
+
}
|
|
96
|
+
return celError("no_such_variable", `no such variable: ${written}`, range);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The dotted chain a select spells, when every step is a plain named member of a **free**
|
|
100
|
+
* name. A name a macro bound is a value, so a chain rooted at one is an ordinary member
|
|
101
|
+
* read — the same rule the checker applies, and the same answer both backends compile.
|
|
102
|
+
*/
|
|
103
|
+
export function plainMemberChain(node, bound) {
|
|
104
|
+
const segments = [];
|
|
105
|
+
let at = node;
|
|
106
|
+
while (at.kind === "select") {
|
|
107
|
+
if (at.optional || at.field === "")
|
|
108
|
+
return undefined;
|
|
109
|
+
segments.unshift(at.field);
|
|
110
|
+
at = at.operand;
|
|
111
|
+
}
|
|
112
|
+
if (at.kind !== "ident")
|
|
113
|
+
return undefined;
|
|
114
|
+
if (!at.absolute && bound(at.name))
|
|
115
|
+
return undefined;
|
|
116
|
+
segments.unshift(at.name);
|
|
117
|
+
return segments;
|
|
118
|
+
}
|
|
119
|
+
// --- member reads ----------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* A member read in every form. Reading **through an optional** answers an optional
|
|
122
|
+
* whichever form the read is written in, which is what lets a chain over a value that
|
|
123
|
+
* may be absent stay one expression: an absent one propagates as absent, and a key the
|
|
124
|
+
* held value does not have is absent too.
|
|
125
|
+
*
|
|
126
|
+
* Which refusals are absence and which are a mistake is `lookupAbsence`'s, and it turns
|
|
127
|
+
* on the FORM the read is written in rather than on what the operand turned out to be: a
|
|
128
|
+
* presence-shaped read over a value that holds no members is absent, while the ordinary
|
|
129
|
+
* read of a member of such a value is the mistake it is outside an optional too.
|
|
130
|
+
*/
|
|
131
|
+
export function readThrough(container, key, optionalForm, range) {
|
|
132
|
+
// The shape nearly every read has — a record's own string-keyed entry — decided by asking
|
|
133
|
+
// what the container is ONCE. The general path below is three total steps, so it reads the
|
|
134
|
+
// type key twice and the prototype again; this asks each question once and falls through
|
|
135
|
+
// to the one seam for every other container, every other key and every refusal.
|
|
136
|
+
if (!optionalForm && typeof key === "string" && typeof container === "object" && container !== null) {
|
|
137
|
+
if (container[CEL_VALUE_TYPE] === undefined) {
|
|
138
|
+
const prototype = Object.getPrototypeOf(container);
|
|
139
|
+
if ((prototype === Object.prototype || prototype === null) &&
|
|
140
|
+
Object.prototype.hasOwnProperty.call(container, key)) {
|
|
141
|
+
const held = container[key];
|
|
142
|
+
// The thenable door, inline: `readHostValue` is this test and nothing else.
|
|
143
|
+
if (typeof held !== "object" || held === null || typeof held.then !== "function") {
|
|
144
|
+
return held;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (isCelOptional(container)) {
|
|
150
|
+
if (!container.present)
|
|
151
|
+
return celNone();
|
|
152
|
+
return optionalRead(container.held, key, optionalForm, range);
|
|
153
|
+
}
|
|
154
|
+
if (optionalForm)
|
|
155
|
+
return optionalRead(container, key, true, range);
|
|
156
|
+
// **A member read is a door a host value comes through**, and the value it answers is
|
|
157
|
+
// whatever the host put inside its own object: `readHostValue` is what refuses a thenable
|
|
158
|
+
// there, rather than the aggregate, the operator or the exit that happens to see it next.
|
|
159
|
+
return readHostValue(celRead(container, key, range), range);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A read that answers an optional. `presence` is whether the read was WRITTEN in a
|
|
163
|
+
* presence-shaped form (`.?`, `[?]`), which is what decides whether a value holding no
|
|
164
|
+
* members is absence or a mistake.
|
|
165
|
+
*/
|
|
166
|
+
function optionalRead(container, key, presence, range) {
|
|
167
|
+
const found = celLookup(container, key);
|
|
168
|
+
if (typeof found !== "symbol") {
|
|
169
|
+
const held = readHostValue(found, range);
|
|
170
|
+
// A value that must be awaited is refused rather than carried as a present optional.
|
|
171
|
+
return isCelError(held) ? held : celSome(held);
|
|
172
|
+
}
|
|
173
|
+
if (lookupAbsence(found, presence))
|
|
174
|
+
return celNone();
|
|
175
|
+
return lookupError(found, key, range);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* `has(a.b)` — presence, which a missing key and a value that holds no members both
|
|
179
|
+
* answer `false` for rather than erroring. An absent optional has no members, and a
|
|
180
|
+
* present one is asked about what it holds.
|
|
181
|
+
*/
|
|
182
|
+
export function hasMember(container, field, range) {
|
|
183
|
+
if (isCelOptional(container)) {
|
|
184
|
+
return container.present ? celHas(container.held, field, range) : false;
|
|
185
|
+
}
|
|
186
|
+
return celHas(container, field, range);
|
|
187
|
+
}
|
|
188
|
+
// --- operands --------------------------------------------------------------
|
|
189
|
+
/** A bool operand, or the error it is — a non-bool is a mistake of its own. */
|
|
190
|
+
export function boolOperand(value, range) {
|
|
191
|
+
if (typeof value === "boolean")
|
|
192
|
+
return value;
|
|
193
|
+
if (isCelError(value))
|
|
194
|
+
return value;
|
|
195
|
+
return celError("no_matching_overload", "this value is not a bool", range);
|
|
196
|
+
}
|
|
197
|
+
/** What an optional entry of an aggregate contributes, or the error it is not one. */
|
|
198
|
+
export function optionalEntry(value, range) {
|
|
199
|
+
if (isCelOptional(value))
|
|
200
|
+
return value;
|
|
201
|
+
return celError("no_matching_overload", "an entry written with '?' holds an optional", range);
|
|
202
|
+
}
|
|
203
|
+
function dispatchOf(name, resolution) {
|
|
204
|
+
if ("resolved" in resolution) {
|
|
205
|
+
const host = resolution.resolved.metadata.implementation;
|
|
206
|
+
if (host)
|
|
207
|
+
return { implementation: host, foreign: true };
|
|
208
|
+
const own = implementationOf(resolution.resolved.signature);
|
|
209
|
+
return own ? { implementation: own, foreign: false } : null;
|
|
210
|
+
}
|
|
211
|
+
// **Equality is universal at runtime**, over every pair of types: the checker refuses
|
|
212
|
+
// `1 == 1u` deliberately, but `dyn(1) == 1u` reaches evaluation and cel-spec answers
|
|
213
|
+
// `true` there. A host's own registration still wins, because it resolved first.
|
|
214
|
+
const equality = equalityFallback(name);
|
|
215
|
+
return equality ? { implementation: equality, foreign: false } : null;
|
|
216
|
+
}
|
|
217
|
+
/** The universal equality, for a pair of types no registration names. */
|
|
218
|
+
function equalityFallback(name) {
|
|
219
|
+
if (name !== "==" && name !== "!=")
|
|
220
|
+
return null;
|
|
221
|
+
return implementationOf({ name, form: "global", parameters: [], returns: DYN }) ?? null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* One call site, and the overloads it has resolved.
|
|
225
|
+
*
|
|
226
|
+
* **Overloads are resolved on the values' own types**, per site: `dyn(1.0) == 1` checks and
|
|
227
|
+
* must then answer across the numeric types, so the statically resolved signature is not
|
|
228
|
+
* enough. A site is almost always monomorphic, so the last resolution is held beside a
|
|
229
|
+
* bounded cache and reached by comparing the type names themselves — building a cache key
|
|
230
|
+
* per call is the allocation that costs most on the hottest path there is. A container's
|
|
231
|
+
* element type is read as `dyn` rather than walked, so dispatch does not get more expensive
|
|
232
|
+
* as the data gets larger.
|
|
233
|
+
*
|
|
234
|
+
* The arguments arrive **already evaluated and already free of errors**: carrying an
|
|
235
|
+
* error-valued operand out is the caller's, because only the caller knows how far it got.
|
|
236
|
+
*/
|
|
237
|
+
export class CallSite {
|
|
238
|
+
name;
|
|
239
|
+
form;
|
|
240
|
+
range;
|
|
241
|
+
registry;
|
|
242
|
+
nominalArity;
|
|
243
|
+
resolved = new BoundedCache(CALL_SITE_CACHE_CAPACITY);
|
|
244
|
+
context;
|
|
245
|
+
/** The arity the last resolution was made under; `-1` until there is one. */
|
|
246
|
+
count = -1;
|
|
247
|
+
t0;
|
|
248
|
+
t1;
|
|
249
|
+
t2;
|
|
250
|
+
t3;
|
|
251
|
+
lastDispatch = null;
|
|
252
|
+
constructor(name, form, range, registry, nominalArity) {
|
|
253
|
+
this.name = name;
|
|
254
|
+
this.form = form;
|
|
255
|
+
this.range = range;
|
|
256
|
+
this.registry = registry;
|
|
257
|
+
this.nominalArity = nominalArity;
|
|
258
|
+
this.context = { range };
|
|
259
|
+
}
|
|
260
|
+
call0() {
|
|
261
|
+
if (this.count === 0)
|
|
262
|
+
return this.answer(this.lastDispatch);
|
|
263
|
+
return this.resolve(0, undefined, undefined, undefined, undefined);
|
|
264
|
+
}
|
|
265
|
+
call1(a) {
|
|
266
|
+
if (this.count === 1 && celTypeNameOf(a) === this.t0)
|
|
267
|
+
return this.answer(this.lastDispatch, a);
|
|
268
|
+
return this.resolve(1, a, undefined, undefined, undefined);
|
|
269
|
+
}
|
|
270
|
+
call2(a, b) {
|
|
271
|
+
if (this.count === 2 && celTypeNameOf(a) === this.t0 && celTypeNameOf(b) === this.t1) {
|
|
272
|
+
return this.answer(this.lastDispatch, a, b);
|
|
273
|
+
}
|
|
274
|
+
return this.resolve(2, a, b, undefined, undefined);
|
|
275
|
+
}
|
|
276
|
+
call3(a, b, c) {
|
|
277
|
+
if (this.count === 3 &&
|
|
278
|
+
celTypeNameOf(a) === this.t0 &&
|
|
279
|
+
celTypeNameOf(b) === this.t1 &&
|
|
280
|
+
celTypeNameOf(c) === this.t2) {
|
|
281
|
+
return this.answer(this.lastDispatch, a, b, c);
|
|
282
|
+
}
|
|
283
|
+
return this.resolve(3, a, b, c, undefined);
|
|
284
|
+
}
|
|
285
|
+
call4(a, b, c, d) {
|
|
286
|
+
if (this.count === 4 &&
|
|
287
|
+
celTypeNameOf(a) === this.t0 &&
|
|
288
|
+
celTypeNameOf(b) === this.t1 &&
|
|
289
|
+
celTypeNameOf(c) === this.t2 &&
|
|
290
|
+
celTypeNameOf(d) === this.t3) {
|
|
291
|
+
return this.answer(this.lastDispatch, a, b, c, d);
|
|
292
|
+
}
|
|
293
|
+
return this.resolve(4, a, b, c, d);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The array form, for a caller that holds its arguments as one — a call written WIDER than
|
|
297
|
+
* any overload can be (`'42'.replace('2', '1', 1, false)` is five values, which is a row),
|
|
298
|
+
* the conformance and identity gates, and a host comparing a site cold against warm. It
|
|
299
|
+
* routes to the same entry points, so there is one dispatch path and not two.
|
|
300
|
+
*/
|
|
301
|
+
call(values) {
|
|
302
|
+
switch (values.length) {
|
|
303
|
+
case 0:
|
|
304
|
+
return this.call0();
|
|
305
|
+
case 1:
|
|
306
|
+
return this.call1(values[0]);
|
|
307
|
+
case 2:
|
|
308
|
+
return this.call2(values[0], values[1]);
|
|
309
|
+
case 3:
|
|
310
|
+
return this.call3(values[0], values[1], values[2]);
|
|
311
|
+
case 4:
|
|
312
|
+
return this.call4(values[0], values[1], values[2], values[3]);
|
|
313
|
+
default:
|
|
314
|
+
return this.wide(values);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A call written with more values than the widest signature may declare. **The arity is
|
|
319
|
+
* the SOURCE's, not the dispatch key's** — anyone may write a call of any width — so this
|
|
320
|
+
* is reachable and answers the ordinary refusal, naming the types it was handed. Nothing
|
|
321
|
+
* resolves here, because a signature that wide is refused where it is registered; the
|
|
322
|
+
* resolution still runs, so one place decides what a call that resolves to nothing says.
|
|
323
|
+
*/
|
|
324
|
+
wide(values) {
|
|
325
|
+
const names = new Array(values.length);
|
|
326
|
+
for (let at = 0; at < values.length; at += 1) {
|
|
327
|
+
const held = celTypeNameOf(values[at]);
|
|
328
|
+
if (held === undefined) {
|
|
329
|
+
const named = readHostValue(values[at], this.range);
|
|
330
|
+
if (isCelError(named))
|
|
331
|
+
return named;
|
|
332
|
+
return celError("no_matching_overload", `${this.name} was handed a value of no CEL type`, this.range);
|
|
333
|
+
}
|
|
334
|
+
names[at] = held;
|
|
335
|
+
}
|
|
336
|
+
return celError("no_matching_overload", `no overload of ${JSON.stringify(this.name)} takes (${names.join(", ")})`, this.range);
|
|
337
|
+
}
|
|
338
|
+
resolve(count, a, b, c, d) {
|
|
339
|
+
const names = new Array(count);
|
|
340
|
+
for (let at = 0; at < count; at += 1) {
|
|
341
|
+
const value = at === 0 ? a : at === 1 ? b : at === 2 ? c : d;
|
|
342
|
+
const held = celTypeNameOf(value);
|
|
343
|
+
if (held === undefined) {
|
|
344
|
+
// A value of no CEL type. A **thenable** is one, and a thenable NESTED inside a host
|
|
345
|
+
// value arrives here rather than through the activation read, because a member read
|
|
346
|
+
// hands back what the host put there — and `resources.x.status.y` is exactly that
|
|
347
|
+
// shape, so this is the door a host actually uses. `readHostValue` names it for what
|
|
348
|
+
// it is; anything else is the overload failure it was going to be. The cost is on the
|
|
349
|
+
// slow path only: dispatch was about to fail either way.
|
|
350
|
+
const named = readHostValue(value, this.range);
|
|
351
|
+
if (isCelError(named))
|
|
352
|
+
return named;
|
|
353
|
+
return celError("no_matching_overload", `${this.name} was handed a value of no CEL type`, this.range);
|
|
354
|
+
}
|
|
355
|
+
names[at] = held;
|
|
356
|
+
}
|
|
357
|
+
const key = names.join(",");
|
|
358
|
+
let dispatch = this.resolved.get(key);
|
|
359
|
+
if (dispatch === undefined) {
|
|
360
|
+
const types = names.map((unused, at) => runtimeType((at === 0 ? a : at === 1 ? b : at === 2 ? c : d), this.nominalArity));
|
|
361
|
+
const resolution = this.registry.resolve(this.name, this.form, this.form === "receiver" ? types.slice(1) : types, this.form === "receiver" ? types[0] : undefined);
|
|
362
|
+
dispatch = dispatchOf(this.name, resolution);
|
|
363
|
+
this.resolved.set(key, dispatch);
|
|
364
|
+
}
|
|
365
|
+
this.count = count;
|
|
366
|
+
this.t0 = names[0];
|
|
367
|
+
this.t1 = names[1];
|
|
368
|
+
this.t2 = names[2];
|
|
369
|
+
this.t3 = names[3];
|
|
370
|
+
this.lastDispatch = dispatch;
|
|
371
|
+
return this.answer(dispatch, a, b, c, d);
|
|
372
|
+
}
|
|
373
|
+
answer(dispatch, a, b, c, d) {
|
|
374
|
+
if (!dispatch) {
|
|
375
|
+
const names = [this.t0, this.t1, this.t2, this.t3].slice(0, Math.max(this.count, 0));
|
|
376
|
+
return celError("no_matching_overload", `no overload of ${JSON.stringify(this.name)} takes (${names.join(", ")})`, this.range);
|
|
377
|
+
}
|
|
378
|
+
const value = dispatch.implementation(this.context, a, b, c, d);
|
|
379
|
+
return dispatch.foreign ? readHostValue(value, this.range) : value;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** A call site over an environment, which both backends build one of per call. */
|
|
383
|
+
export function callSiteOf(target, name, form, range) {
|
|
384
|
+
return new CallSite(name, form, range, target.registry, target.nominalArity);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* The type of a value, for dispatch. A container's element type is `dyn`: reading it
|
|
388
|
+
* exactly would mean walking the data on every call, and the registry's loose pass
|
|
389
|
+
* resolves a parameterized overload against `dyn` anyway.
|
|
390
|
+
*/
|
|
391
|
+
export function runtimeType(value, nominalArity) {
|
|
392
|
+
const name = celTypeNameOf(value);
|
|
393
|
+
switch (name) {
|
|
394
|
+
case "int":
|
|
395
|
+
return INT;
|
|
396
|
+
case "uint":
|
|
397
|
+
return UINT;
|
|
398
|
+
case "double":
|
|
399
|
+
return DOUBLE;
|
|
400
|
+
case "bool":
|
|
401
|
+
return BOOL;
|
|
402
|
+
case "string":
|
|
403
|
+
return STRING;
|
|
404
|
+
case "bytes":
|
|
405
|
+
return BYTES;
|
|
406
|
+
case "null_type":
|
|
407
|
+
return NULL;
|
|
408
|
+
case "type":
|
|
409
|
+
return TYPE;
|
|
410
|
+
case "google.protobuf.Timestamp":
|
|
411
|
+
return TIMESTAMP;
|
|
412
|
+
case "google.protobuf.Duration":
|
|
413
|
+
return DURATION;
|
|
414
|
+
case "list":
|
|
415
|
+
return listOf(DYN);
|
|
416
|
+
case "map":
|
|
417
|
+
return mapOf(DYN, DYN);
|
|
418
|
+
case "optional":
|
|
419
|
+
return optionalOf(DYN);
|
|
420
|
+
default: {
|
|
421
|
+
const arity = nominalArity(name) ?? 0;
|
|
422
|
+
return { kind: "nominal", name: name, base: DYN, args: Array.from({ length: arity }, () => DYN) };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
export declare class BoundedCache<Key, Value> {
|
|
12
|
+
readonly capacity: number;
|
|
13
|
+
private readonly held;
|
|
14
|
+
constructor(capacity: number);
|
|
15
|
+
get(key: Key): Value | undefined;
|
|
16
|
+
set(key: Key, value: Value): void;
|
|
17
|
+
/** Forgets everything — what a registration does to an environment's compiled programs. */
|
|
18
|
+
clear(): void;
|
|
19
|
+
get size(): number;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=bounded-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounded-cache.d.ts","sourceRoot":"","sources":["../src/bounded-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,qBAAa,YAAY,CAAC,GAAG,EAAE,KAAK;IAGtB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAFrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;gBAEzB,QAAQ,EAAE,MAAM;IAMrC,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,KAAK,GAAG,SAAS;IAQhC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAQjC,2FAA2F;IAC3F,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
export class BoundedCache {
|
|
12
|
+
capacity;
|
|
13
|
+
held = new Map();
|
|
14
|
+
constructor(capacity) {
|
|
15
|
+
this.capacity = capacity;
|
|
16
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
17
|
+
throw new RangeError("a bounded cache holds at least one entry");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
get(key) {
|
|
21
|
+
if (!this.held.has(key))
|
|
22
|
+
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
|
+
set(key, value) {
|
|
29
|
+
this.held.delete(key);
|
|
30
|
+
this.held.set(key, value);
|
|
31
|
+
if (this.held.size > this.capacity) {
|
|
32
|
+
this.held.delete(this.held.keys().next().value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Forgets everything — what a registration does to an environment's compiled programs. */
|
|
36
|
+
clear() {
|
|
37
|
+
this.held.clear();
|
|
38
|
+
}
|
|
39
|
+
get size() {
|
|
40
|
+
return this.held.size;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every function of the catalog **does** — once, for both backends.
|
|
3
|
+
*
|
|
4
|
+
* The declarations are data (`signatures/function-catalog.json`); this is the behaviour
|
|
5
|
+
* behind them, keyed by the same dispatch key the registry resolves on, exactly as
|
|
6
|
+
* `runtime-library.ts` is for CEL's own library. Both backends reach it through the
|
|
7
|
+
* registration's `implementation`, so there is one answer per operation and no second
|
|
8
|
+
* copy to drift.
|
|
9
|
+
*
|
|
10
|
+
* **Nine functions are the HOST's**, not this engine's: the four hashes, `hmac`, the two
|
|
11
|
+
* base64 string conversions, `json` and `joinPath`. Each needs a platform facility this
|
|
12
|
+
* package may not reach — a hash, a byte buffer, the host's own path separator — so the
|
|
13
|
+
* host supplies it through `CelCatalogHandlers` and a registration made without one
|
|
14
|
+
* answers an `unbound_function` error **naming the function**. Never a null, and never a
|
|
15
|
+
* silent empty string: a hash that answers nothing looks like a value and ends up in a
|
|
16
|
+
* cache key.
|
|
17
|
+
*
|
|
18
|
+
* **A refusal is worded by the catalog, in one voice** — `<function>: <what is wrong>` —
|
|
19
|
+
* and never by a library or by the host language's runtime. It is thrown as
|
|
20
|
+
* `CatalogRefusal` by the guard that meets it and turned into a CEL **error value** at
|
|
21
|
+
* exactly one place (`refusing`), so the short-circuit rules still hold and the static
|
|
22
|
+
* guard below reports the same sentence the evaluation would. Anything else thrown while
|
|
23
|
+
* an implementation runs is this engine failing, not the argument, and propagates.
|
|
24
|
+
*/
|
|
25
|
+
import type { CelImplementation } from "./runtime-library.js";
|
|
26
|
+
import type { LiteralArgumentCheck } from "./signature.js";
|
|
27
|
+
import type { CelValue } from "./cel-value.js";
|
|
28
|
+
/**
|
|
29
|
+
* The nine functions the host answers for. Each needs a facility this package may not
|
|
30
|
+
* reach, which is the whole of why it is a seam: a hash and a byte buffer are the host
|
|
31
|
+
* platform's, and what a path separator IS belongs to the machine running the app.
|
|
32
|
+
*/
|
|
33
|
+
export interface CelCatalogHandlers {
|
|
34
|
+
sha256: (text: string) => string;
|
|
35
|
+
md5: (text: string) => string;
|
|
36
|
+
sha1: (text: string) => string;
|
|
37
|
+
sha512: (text: string) => string;
|
|
38
|
+
hmac: (algorithm: string, key: string, message: string) => string;
|
|
39
|
+
base64Encode: (text: string) => string;
|
|
40
|
+
base64Decode: (text: string) => string;
|
|
41
|
+
json: (value: CelValue) => string;
|
|
42
|
+
/** Join a relative path onto a base with the HOST's path rules. */
|
|
43
|
+
joinPath: (base: string, relative: string) => string;
|
|
44
|
+
}
|
|
45
|
+
/** An implementation, once the host's handlers are known. */
|
|
46
|
+
export type CatalogImplementation = (handlers: Partial<CelCatalogHandlers>) => CelImplementation;
|
|
47
|
+
/**
|
|
48
|
+
* The implementation of a catalog signature, given the host's handlers, or nothing where
|
|
49
|
+
* the catalog declares no such call. A declaration with no behaviour here is refused at
|
|
50
|
+
* registration rather than at evaluation.
|
|
51
|
+
*/
|
|
52
|
+
export declare function catalogImplementation(key: string, handlers: Partial<CelCatalogHandlers>): CelImplementation | undefined;
|
|
53
|
+
/** Every dispatch key this file answers for, for a completeness check. */
|
|
54
|
+
export declare function catalogImplementedKeys(): readonly string[];
|
|
55
|
+
/** The literal guard of a catalog function, where it has one. */
|
|
56
|
+
export declare function catalogLiteralCheck(name: string): LiteralArgumentCheck | undefined;
|
|
57
|
+
/** Every function name a literal guard answers for, for a completeness check. */
|
|
58
|
+
export declare function catalogGuardedNames(): readonly string[];
|
|
59
|
+
//# sourceMappingURL=catalog-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-runtime.d.ts","sourceRoot":"","sources":["../src/catalog-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAOV,QAAQ,EACT,MAAM,gBAAgB,CAAC;AA6BxB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,IAAI,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,MAAM,CAAC;IAClC,mEAAmE;IACnE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CACtD;AAED,6DAA6D;AAC7D,MAAM,MAAM,qBAAqB,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,kBAAkB,CAAC,KAAK,iBAAiB,CAAC;AAstBjG;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,OAAO,CAAC,kBAAkB,CAAC,GACpC,iBAAiB,GAAG,SAAS,CAE/B;AAED,0EAA0E;AAC1E,wBAAgB,sBAAsB,IAAI,SAAS,MAAM,EAAE,CAE1D;AAkFD,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAElF;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,IAAI,SAAS,MAAM,EAAE,CAEvD"}
|