@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,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One CEL expression, read and resolved.
|
|
3
|
+
*
|
|
4
|
+
* This is the front end's whole answer: the source as written, the canonical tree,
|
|
5
|
+
* the namespace set the tree was resolved under, and at most one syntax diagnostic.
|
|
6
|
+
* **Every expression this module hands out is already resolved** — there is no way
|
|
7
|
+
* to obtain an unresolved tree and then forget to resolve it, and `namespaces`
|
|
8
|
+
* records what it was resolved under so a consumer checking it against a different
|
|
9
|
+
* set can refuse rather than quietly answer the wrong question.
|
|
10
|
+
*
|
|
11
|
+
* Reading an expression never throws: a source it cannot read gives a tree for the
|
|
12
|
+
* longest prefix it could, plus the diagnostic saying where it stopped.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
namespaceSetsEqual,
|
|
17
|
+
normalizeNamespaces,
|
|
18
|
+
resolveNamespaces,
|
|
19
|
+
} from "./namespace-resolution.js";
|
|
20
|
+
import type { ParseOptions } from "./parser.js";
|
|
21
|
+
import { parseSyntax } from "./parser.js";
|
|
22
|
+
import type { CelSyntaxDiagnostic } from "./syntax-diagnostic.js";
|
|
23
|
+
import type { CelNode } from "./syntax-tree.js";
|
|
24
|
+
|
|
25
|
+
export interface CelExpression {
|
|
26
|
+
readonly source: string;
|
|
27
|
+
readonly root: CelNode;
|
|
28
|
+
/** The namespace set the tree was resolved under, in canonical order. */
|
|
29
|
+
readonly namespaces: readonly string[];
|
|
30
|
+
/** At most one entry; one exactly when the source could not be read whole. */
|
|
31
|
+
readonly diagnostics: readonly CelSyntaxDiagnostic[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ParseExpressionOptions extends ParseOptions {
|
|
35
|
+
/** The names that denote namespaces rather than values at this site. */
|
|
36
|
+
readonly namespaces?: Iterable<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseExpression(source: string, options?: ParseExpressionOptions): CelExpression {
|
|
40
|
+
const namespaces = normalizeNamespaces(options?.namespaces ?? []);
|
|
41
|
+
const parsed = parseSyntax(source, options);
|
|
42
|
+
return {
|
|
43
|
+
source,
|
|
44
|
+
root: resolveNamespaces(parsed.root, namespaces),
|
|
45
|
+
namespaces,
|
|
46
|
+
diagnostics: parsed.diagnostics,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether the expression was resolved under exactly these namespaces. */
|
|
51
|
+
export function resolvedUnder(expression: CelExpression, namespaces: Iterable<string>): boolean {
|
|
52
|
+
return namespaceSetsEqual(expression.namespaces, normalizeNamespaces(namespaces));
|
|
53
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building a CEL map, and the one identity a key has.
|
|
3
|
+
*
|
|
4
|
+
* A map's entries live in a `Map` keyed by each key's **own typed value**, never by a
|
|
5
|
+
* property name. Three consequences, all load-bearing:
|
|
6
|
+
*
|
|
7
|
+
* - A map holds CEL's four key types (int, uint, bool, string) in one container, and an
|
|
8
|
+
* int key and a uint key of the same number are **one** key, because CEL equality
|
|
9
|
+
* makes them equal — so a literal writing both is a duplicate rather than two entries.
|
|
10
|
+
* - Nothing a key spells can reach a prototype, a method or an inherited member. A map
|
|
11
|
+
* built from `{'__proto__': 1}` holds `__proto__` as data and reads it back as data;
|
|
12
|
+
* dropping such a key would be swallowing what the author wrote.
|
|
13
|
+
* - The types separate themselves. A `Map` compares its keys by type as well as value, so
|
|
14
|
+
* a string `"1"` and an int `1` are distinct keys with nothing prefixed to say so — and a
|
|
15
|
+
* second engine keys an entry by its own discriminant rather than reproducing this one's
|
|
16
|
+
* text byte for byte.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { CelMap, CelMapKey, CelMapValueEntry, CelValue } from "./cel-value.js";
|
|
20
|
+
import {
|
|
21
|
+
CEL_VALUE_TYPE,
|
|
22
|
+
celError,
|
|
23
|
+
isCelError,
|
|
24
|
+
isCelUint,
|
|
25
|
+
type CelError,
|
|
26
|
+
} from "./cel-value.js";
|
|
27
|
+
import type { SourceRange } from "./syntax-tree.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What identifies a key. The `bigint` covers the numeric types together: CEL equality
|
|
31
|
+
* makes `1`, `1u` and `1.0` one key, so a map carrying two of them is not a map and a
|
|
32
|
+
* lookup by any of the three finds the entry — which is what
|
|
33
|
+
* `{1u: 1.0, 2: 2.0, 3u: 3.0}[?3.0]` reads. A double that is not whole identifies
|
|
34
|
+
* nothing, since no entry can hold it.
|
|
35
|
+
*
|
|
36
|
+
* A string and a bool identify themselves, and nothing is prefixed to keep the types
|
|
37
|
+
* apart: a `Map` key of one type never equals a key of another, so `"1"` and `1` are
|
|
38
|
+
* distinct keys and `"true"` is not `true`.
|
|
39
|
+
*/
|
|
40
|
+
export function mapKeyIdentity(key: CelValue): CelMapKey | undefined {
|
|
41
|
+
const held = typeof key;
|
|
42
|
+
if (held === "string" || held === "boolean" || held === "bigint") return key as CelMapKey;
|
|
43
|
+
if (held === "number") return Number.isInteger(key) ? BigInt(key as number) : undefined;
|
|
44
|
+
return isCelUint(key) ? key.value : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** An empty map, for a literal with no entries. */
|
|
48
|
+
export function celMapOf(entries: ReadonlyMap<CelMapKey, CelMapValueEntry>): CelMap {
|
|
49
|
+
return { [CEL_VALUE_TYPE]: "map", entries };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A map from its entries in written order, or the error that stops it: a key of a type
|
|
54
|
+
* no map is keyed by, or two keys CEL equality makes one.
|
|
55
|
+
*
|
|
56
|
+
* The entries arrive **flat** — key, value, key, value — because a literal of n entries
|
|
57
|
+
* then costs one allocation rather than one per entry, and both backends write the same
|
|
58
|
+
* call. A duplicate is caught by the size not moving, which is the one hash lookup the
|
|
59
|
+
* insert already performs.
|
|
60
|
+
*/
|
|
61
|
+
export function celMapFromEntries(flat: readonly CelValue[], range?: SourceRange): CelMap | CelError {
|
|
62
|
+
const entries = new Map<CelMapKey, CelMapValueEntry>();
|
|
63
|
+
for (let at = 0; at < flat.length; at += 2) {
|
|
64
|
+
const key = flat[at] as CelValue;
|
|
65
|
+
const value = flat[at + 1] as CelValue;
|
|
66
|
+
if (isCelError(key)) return key;
|
|
67
|
+
if (isCelError(value)) return value;
|
|
68
|
+
// A map is **built** with an int, uint, bool or string key: a double is not a key
|
|
69
|
+
// type, even one that is whole. It still LOOKS one up, because `{1u: …}[?1.0]` reads
|
|
70
|
+
// the entry — equality across the numeric types holds for a key as for a value.
|
|
71
|
+
const identity = typeof key === "number" ? undefined : mapKeyIdentity(key);
|
|
72
|
+
if (identity === undefined) {
|
|
73
|
+
return celError(
|
|
74
|
+
"unsupported_key_type",
|
|
75
|
+
"a map is keyed by an int, a uint, a bool or a string",
|
|
76
|
+
range,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const before = entries.size;
|
|
80
|
+
entries.set(identity, { key, value });
|
|
81
|
+
if (entries.size === before) {
|
|
82
|
+
return celError("duplicate_map_key", `the key ${describeKey(key)} is written twice`, range);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return celMapOf(entries);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function describeKey(key: CelValue): string {
|
|
89
|
+
if (typeof key === "string") return JSON.stringify(key);
|
|
90
|
+
if (isCelUint(key)) return `${key.value}u`;
|
|
91
|
+
return String(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Every key of a map, in insertion order — what a comprehension ranges over. */
|
|
95
|
+
export function celMapKeys(map: CelMap): CelValue[] {
|
|
96
|
+
return [...map.entries.values()].map((entry) => entry.key);
|
|
97
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A compiled expression, and what happens at the top of an evaluation.
|
|
3
|
+
*
|
|
4
|
+
* An error is a **value** throughout evaluation, so that `false && <missing key>` is
|
|
5
|
+
* `false`. At the top there is nothing left to short-circuit, so a surviving error
|
|
6
|
+
* becomes a thrown failure carrying its code — the one place the engine throws about an
|
|
7
|
+
* expression's own data, and the only form a caller can be expected to handle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CelActivation } from "./activation.js";
|
|
11
|
+
import type { CelStep, CompileTarget, NamespaceDispatch } from "./backend-runtime.js";
|
|
12
|
+
import { compileTree } from "./closure-backend.js";
|
|
13
|
+
import type { CelError, CelEvaluationCode, CelValue } from "./cel-value.js";
|
|
14
|
+
import { celError, isCelError, isThenable } from "./cel-value.js";
|
|
15
|
+
import type { CelExpression } from "./cel-expression.js";
|
|
16
|
+
import type { SourceRange } from "./syntax-tree.js";
|
|
17
|
+
|
|
18
|
+
/** A failure an evaluation could not carry any further. */
|
|
19
|
+
export class CelEvaluationError extends Error {
|
|
20
|
+
readonly code: CelEvaluationCode;
|
|
21
|
+
readonly range?: SourceRange;
|
|
22
|
+
|
|
23
|
+
constructor(code: CelEvaluationCode, message: string, range?: SourceRange) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "CelEvaluationError";
|
|
26
|
+
this.code = code;
|
|
27
|
+
if (range) this.range = range;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EvaluateOptions {
|
|
32
|
+
/**
|
|
33
|
+
* What a namespaced call (`Alias.fn(x)`) is dispatched through. A host binds it per
|
|
34
|
+
* evaluation, because which functions a scope reaches is the host's own question; a
|
|
35
|
+
* call nothing binds is `unbound_function`.
|
|
36
|
+
*/
|
|
37
|
+
readonly namespaceFunction?: NamespaceDispatch;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CelProgram {
|
|
41
|
+
readonly source: string;
|
|
42
|
+
/** The value, or a throw carrying the code of the error that survived to the top. */
|
|
43
|
+
evaluate(activation?: CelActivation, options?: EvaluateOptions): CelValue;
|
|
44
|
+
/** The value or the error value, for a caller that carries errors itself. */
|
|
45
|
+
evaluateToValue(activation?: CelActivation, options?: EvaluateOptions): CelValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The refusal a thenable surviving to the top is, spanning the whole expression. */
|
|
49
|
+
function asyncRefused(source: string): CelError {
|
|
50
|
+
return celError(
|
|
51
|
+
"async_value_unsupported",
|
|
52
|
+
"a value that must be awaited reached evaluation, and CEL evaluates synchronously",
|
|
53
|
+
[0, source.length],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NO_ACTIVATION: CelActivation = Object.freeze(Object.create(null) as CelActivation);
|
|
58
|
+
/** A frame that binds nothing needs no slots, and allocating none is measurable. */
|
|
59
|
+
const NO_SLOTS: CelValue[] = [];
|
|
60
|
+
|
|
61
|
+
export function compileExpression(expression: CelExpression, target: CompileTarget): CelProgram {
|
|
62
|
+
const compiled = compileTree(expression.root, target);
|
|
63
|
+
return new Program(expression.source, compiled.step, compiled.slots);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A program over a step a backend already built — what an emitted module's function
|
|
68
|
+
* becomes. The top of an evaluation is the same either way: the thenable backstop, and a
|
|
69
|
+
* surviving error turning into a throw. A second implementation of that top would be a
|
|
70
|
+
* second answer to "what does a caller see", which is the one thing two backends may not
|
|
71
|
+
* disagree about.
|
|
72
|
+
*/
|
|
73
|
+
export function programOfStep(source: string, step: CelStep, slots = 0): CelProgram {
|
|
74
|
+
return new Program(source, step, slots);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class Program implements CelProgram {
|
|
78
|
+
constructor(
|
|
79
|
+
readonly source: string,
|
|
80
|
+
private readonly step: CelStep,
|
|
81
|
+
private readonly slots: number,
|
|
82
|
+
) {}
|
|
83
|
+
|
|
84
|
+
evaluateToValue(activation: CelActivation = NO_ACTIVATION, options: EvaluateOptions = {}): CelValue {
|
|
85
|
+
// One shape, every time: a frame built two ways is two shapes, and every step that
|
|
86
|
+
// reads one would see both.
|
|
87
|
+
const value = this.step({
|
|
88
|
+
activation,
|
|
89
|
+
slots: this.slots === 0 ? NO_SLOTS : new Array<CelValue>(this.slots),
|
|
90
|
+
namespaceFunction: options.namespaceFunction,
|
|
91
|
+
});
|
|
92
|
+
// The backstop, one check per evaluation: every door refuses a thenable where the value
|
|
93
|
+
// enters, and this is what catches a door nobody has thought of yet rather than handing a
|
|
94
|
+
// promise to a caller as though it were a value.
|
|
95
|
+
return isThenable(value) ? asyncRefused(this.source) : value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
evaluate(activation?: CelActivation, options?: EvaluateOptions): CelValue {
|
|
99
|
+
const value = this.evaluateToValue(activation, options);
|
|
100
|
+
if (isCelError(value)) throw new CelEvaluationError(value.code, value.message, value.range);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/cel-type.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CEL's types.
|
|
3
|
+
*
|
|
4
|
+
* The domain is deliberately richer than the one an expression can spell, because
|
|
5
|
+
* the checker's whole value is in what it can be *told*: a variable typed from a
|
|
6
|
+
* JSON Schema carries its nested objects, its element types and its unions, and a
|
|
7
|
+
* typo two levels down is then a type error rather than something only a running
|
|
8
|
+
* request discovers.
|
|
9
|
+
*
|
|
10
|
+
* Four shapes exist for that reason and have no syntax of their own:
|
|
11
|
+
*
|
|
12
|
+
* - `record` — an object with named fields, each typed in turn; `open` says whether
|
|
13
|
+
* a field it does not declare is unjudged (a schema with no `properties`) or
|
|
14
|
+
* undeclared (a closed schema).
|
|
15
|
+
* - `union` — carried as a union rather than collapsed to `dyn`, so `int|null` keeps
|
|
16
|
+
* both the arithmetic and the null that makes a dereference worth reporting.
|
|
17
|
+
* - `nominal` — a named type over a base, with invariant arguments: the mechanism a
|
|
18
|
+
* host registers its own vocabulary through. **A nominal is not its base**: it
|
|
19
|
+
* accepts only what is registered for it, which is what makes a slot expecting one
|
|
20
|
+
* refuse a plain value of the base. (The vectors pin this: an operator the base
|
|
21
|
+
* accepts is refused on the named type.)
|
|
22
|
+
* - `parameter` — a type variable in a signature, unified per call and invariant.
|
|
23
|
+
*
|
|
24
|
+
* `dyn` is the top: it is assignable both ways, which is what keeps an unlisted
|
|
25
|
+
* variable or an unknown call result from cascading into a second error.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type PrimitiveName =
|
|
29
|
+
| "int"
|
|
30
|
+
| "uint"
|
|
31
|
+
| "double"
|
|
32
|
+
| "bool"
|
|
33
|
+
| "string"
|
|
34
|
+
| "bytes"
|
|
35
|
+
| "null"
|
|
36
|
+
| "type"
|
|
37
|
+
| "timestamp"
|
|
38
|
+
| "duration";
|
|
39
|
+
|
|
40
|
+
export interface DynType {
|
|
41
|
+
readonly kind: "dyn";
|
|
42
|
+
}
|
|
43
|
+
export interface PrimitiveType {
|
|
44
|
+
readonly kind: "primitive";
|
|
45
|
+
readonly name: PrimitiveName;
|
|
46
|
+
}
|
|
47
|
+
export interface ListType {
|
|
48
|
+
readonly kind: "list";
|
|
49
|
+
readonly element: CelType;
|
|
50
|
+
}
|
|
51
|
+
export interface MapType {
|
|
52
|
+
readonly kind: "map";
|
|
53
|
+
readonly key: CelType;
|
|
54
|
+
readonly value: CelType;
|
|
55
|
+
}
|
|
56
|
+
export interface OptionalType {
|
|
57
|
+
readonly kind: "optional";
|
|
58
|
+
readonly value: CelType;
|
|
59
|
+
}
|
|
60
|
+
export interface RecordType {
|
|
61
|
+
readonly kind: "record";
|
|
62
|
+
readonly name?: string;
|
|
63
|
+
readonly fields: ReadonlyMap<string, CelType>;
|
|
64
|
+
/** A field the record does not declare is unjudged when open, undeclared when closed. */
|
|
65
|
+
readonly open: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface UnionType {
|
|
68
|
+
readonly kind: "union";
|
|
69
|
+
readonly members: readonly CelType[];
|
|
70
|
+
}
|
|
71
|
+
export interface NominalType {
|
|
72
|
+
readonly kind: "nominal";
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly base: CelType;
|
|
75
|
+
readonly args: readonly CelType[];
|
|
76
|
+
}
|
|
77
|
+
export interface ParameterType {
|
|
78
|
+
readonly kind: "parameter";
|
|
79
|
+
readonly name: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type CelType =
|
|
83
|
+
| DynType
|
|
84
|
+
| PrimitiveType
|
|
85
|
+
| ListType
|
|
86
|
+
| MapType
|
|
87
|
+
| OptionalType
|
|
88
|
+
| RecordType
|
|
89
|
+
| UnionType
|
|
90
|
+
| NominalType
|
|
91
|
+
| ParameterType;
|
|
92
|
+
|
|
93
|
+
export const DYN: DynType = { kind: "dyn" };
|
|
94
|
+
|
|
95
|
+
const primitive = (name: PrimitiveName): PrimitiveType => ({ kind: "primitive", name });
|
|
96
|
+
|
|
97
|
+
export const INT = primitive("int");
|
|
98
|
+
export const UINT = primitive("uint");
|
|
99
|
+
export const DOUBLE = primitive("double");
|
|
100
|
+
export const BOOL = primitive("bool");
|
|
101
|
+
export const STRING = primitive("string");
|
|
102
|
+
export const BYTES = primitive("bytes");
|
|
103
|
+
export const NULL = primitive("null");
|
|
104
|
+
export const TYPE = primitive("type");
|
|
105
|
+
export const TIMESTAMP = primitive("timestamp");
|
|
106
|
+
export const DURATION = primitive("duration");
|
|
107
|
+
|
|
108
|
+
export const listOf = (element: CelType): ListType => ({ kind: "list", element });
|
|
109
|
+
export const mapOf = (key: CelType, value: CelType): MapType => ({ kind: "map", key, value });
|
|
110
|
+
export const optionalOf = (value: CelType): OptionalType => ({ kind: "optional", value });
|
|
111
|
+
export const parameterOf = (name: string): ParameterType => ({ kind: "parameter", name });
|
|
112
|
+
|
|
113
|
+
/** A union of its distinct members; one member is that member, none is `dyn`. */
|
|
114
|
+
export function unionOf(members: readonly CelType[]): CelType {
|
|
115
|
+
const flat: CelType[] = [];
|
|
116
|
+
for (const member of members) {
|
|
117
|
+
for (const part of member.kind === "union" ? member.members : [member]) {
|
|
118
|
+
if (!flat.some((held) => typesEqual(held, part))) flat.push(part);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (flat.length === 0) return DYN;
|
|
122
|
+
if (flat.length === 1) return flat[0]!;
|
|
123
|
+
if (flat.some((member) => member.kind === "dyn")) return DYN;
|
|
124
|
+
return { kind: "union", members: flat };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The names the timestamp and duration types are written and printed under. */
|
|
128
|
+
export const WELL_KNOWN_TYPE_NAMES: Readonly<Record<string, PrimitiveName>> = {
|
|
129
|
+
"google.protobuf.Timestamp": "timestamp",
|
|
130
|
+
"google.protobuf.Duration": "duration",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const PRIMITIVE_PRINTED: Readonly<Record<PrimitiveName, string>> = {
|
|
134
|
+
int: "int",
|
|
135
|
+
uint: "uint",
|
|
136
|
+
double: "double",
|
|
137
|
+
bool: "bool",
|
|
138
|
+
string: "string",
|
|
139
|
+
bytes: "bytes",
|
|
140
|
+
null: "null",
|
|
141
|
+
type: "type",
|
|
142
|
+
timestamp: "google.protobuf.Timestamp",
|
|
143
|
+
duration: "google.protobuf.Duration",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export function isDyn(type: CelType): boolean {
|
|
147
|
+
return type.kind === "dyn";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function typesEqual(left: CelType, right: CelType): boolean {
|
|
151
|
+
if (left.kind !== right.kind) return false;
|
|
152
|
+
switch (left.kind) {
|
|
153
|
+
case "dyn":
|
|
154
|
+
return true;
|
|
155
|
+
case "primitive":
|
|
156
|
+
return left.name === (right as PrimitiveType).name;
|
|
157
|
+
case "list":
|
|
158
|
+
return typesEqual(left.element, (right as ListType).element);
|
|
159
|
+
case "map": {
|
|
160
|
+
const other = right as MapType;
|
|
161
|
+
return typesEqual(left.key, other.key) && typesEqual(left.value, other.value);
|
|
162
|
+
}
|
|
163
|
+
case "optional":
|
|
164
|
+
return typesEqual(left.value, (right as OptionalType).value);
|
|
165
|
+
case "record": {
|
|
166
|
+
const other = right as RecordType;
|
|
167
|
+
if (left.name !== other.name || left.open !== other.open || left.fields.size !== other.fields.size) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
for (const [field, type] of left.fields) {
|
|
171
|
+
const held = other.fields.get(field);
|
|
172
|
+
if (!held || !typesEqual(type, held)) return false;
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
case "union": {
|
|
177
|
+
const other = right as UnionType;
|
|
178
|
+
return (
|
|
179
|
+
left.members.length === other.members.length &&
|
|
180
|
+
left.members.every((member, at) => typesEqual(member, other.members[at]!))
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
case "nominal": {
|
|
184
|
+
const other = right as NominalType;
|
|
185
|
+
return (
|
|
186
|
+
left.name === other.name &&
|
|
187
|
+
left.args.length === other.args.length &&
|
|
188
|
+
left.args.every((argument, at) => typesEqual(argument, other.args[at]!))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
case "parameter":
|
|
192
|
+
return left.name === (right as ParameterType).name;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* How a type is written. `top` collapses a list or map of `dyn` to its bare name,
|
|
198
|
+
* which is how the whole type of an expression is reported.
|
|
199
|
+
*/
|
|
200
|
+
export function formatType(type: CelType, top = false): string {
|
|
201
|
+
switch (type.kind) {
|
|
202
|
+
case "dyn":
|
|
203
|
+
return "dyn";
|
|
204
|
+
case "primitive":
|
|
205
|
+
return PRIMITIVE_PRINTED[type.name];
|
|
206
|
+
case "list":
|
|
207
|
+
return top && isDyn(type.element) ? "list" : `list<${formatType(type.element)}>`;
|
|
208
|
+
case "map":
|
|
209
|
+
return top && isDyn(type.key) && isDyn(type.value)
|
|
210
|
+
? "map"
|
|
211
|
+
: `map<${formatType(type.key)}, ${formatType(type.value)}>`;
|
|
212
|
+
case "optional":
|
|
213
|
+
return `optional<${formatType(type.value)}>`;
|
|
214
|
+
case "record":
|
|
215
|
+
// An unnamed record is the shape a map has at runtime, and that is how a
|
|
216
|
+
// message names it; its fields are what a diagnostic lists, not its type.
|
|
217
|
+
return type.name ?? "map";
|
|
218
|
+
case "union":
|
|
219
|
+
return type.members.map((member) => formatType(member)).join("|");
|
|
220
|
+
case "nominal":
|
|
221
|
+
return type.args.length === 0
|
|
222
|
+
? type.name
|
|
223
|
+
: `${type.name}<${type.args.map((argument) => formatType(argument)).join(", ")}>`;
|
|
224
|
+
case "parameter":
|
|
225
|
+
return type.name;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The numeric types, over which CEL compares and converts across type. */
|
|
230
|
+
const NUMERIC = new Set<PrimitiveName>(["int", "uint", "double"]);
|
|
231
|
+
|
|
232
|
+
export function isNumeric(type: CelType): boolean {
|
|
233
|
+
return type.kind === "primitive" && NUMERIC.has(type.name);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The type with every unresolved type parameter replaced by `dyn` — the last step before a
|
|
238
|
+
* type is REPORTED.
|
|
239
|
+
*
|
|
240
|
+
* cel-spec's rule is that an unresolved parameter behaves as `dyn` wherever it is used, and
|
|
241
|
+
* the type handed out is a use like any other: a consumer that reads `list<T>` has to know
|
|
242
|
+
* what `T` means to this engine, and the answer is "nothing — it was never resolved". A
|
|
243
|
+
* parameter survives only where it is DECLARED: a signature's text, a nominal type's
|
|
244
|
+
* parameter list.
|
|
245
|
+
*/
|
|
246
|
+
export function withoutParameters(type: CelType): CelType {
|
|
247
|
+
switch (type.kind) {
|
|
248
|
+
case "parameter":
|
|
249
|
+
return DYN;
|
|
250
|
+
case "list": {
|
|
251
|
+
const element = withoutParameters(type.element);
|
|
252
|
+
return element === type.element ? type : listOf(element);
|
|
253
|
+
}
|
|
254
|
+
case "map": {
|
|
255
|
+
const key = withoutParameters(type.key);
|
|
256
|
+
const value = withoutParameters(type.value);
|
|
257
|
+
return key === type.key && value === type.value ? type : mapOf(key, value);
|
|
258
|
+
}
|
|
259
|
+
case "optional": {
|
|
260
|
+
const value = withoutParameters(type.value);
|
|
261
|
+
return value === type.value ? type : optionalOf(value);
|
|
262
|
+
}
|
|
263
|
+
case "union":
|
|
264
|
+
return unionOf(type.members.map(withoutParameters));
|
|
265
|
+
case "nominal": {
|
|
266
|
+
const args = type.args.map(withoutParameters);
|
|
267
|
+
return args.every((argument, at) => argument === type.args[at]) ? type : { ...type, args };
|
|
268
|
+
}
|
|
269
|
+
case "record": {
|
|
270
|
+
let moved = false;
|
|
271
|
+
const fields = new Map<string, CelType>();
|
|
272
|
+
for (const [name, held] of type.fields) {
|
|
273
|
+
const replaced = withoutParameters(held);
|
|
274
|
+
if (replaced !== held) moved = true;
|
|
275
|
+
fields.set(name, replaced);
|
|
276
|
+
}
|
|
277
|
+
return moved ? { ...type, fields } : type;
|
|
278
|
+
}
|
|
279
|
+
default:
|
|
280
|
+
return type;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Whether `type` admits a null value — a null, or a union holding one. */
|
|
285
|
+
export function admitsNull(type: CelType): boolean {
|
|
286
|
+
if (type.kind === "primitive") return type.name === "null";
|
|
287
|
+
if (type.kind === "union") return type.members.some(admitsNull);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The type with every null taken out of it; `dyn` and a bare null are unchanged. */
|
|
292
|
+
export function withoutNull(type: CelType): CelType {
|
|
293
|
+
if (type.kind !== "union") return type;
|
|
294
|
+
const members = type.members.filter((member) => !admitsNull(member));
|
|
295
|
+
return members.length === 0 ? type : unionOf(members);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether a value of `from` may stand where `to` is wanted.
|
|
300
|
+
*
|
|
301
|
+
* `dyn` passes both ways; a nominal type accepts only itself (its base is a
|
|
302
|
+
* different type); a union passes where every member does, and accepts a value any
|
|
303
|
+
* member accepts. Numeric types do **not** convert implicitly — CEL's cross-type
|
|
304
|
+
* arithmetic and comparison are declared overloads, not coercions.
|
|
305
|
+
*/
|
|
306
|
+
export function assignable(from: CelType, to: CelType): boolean {
|
|
307
|
+
if (isDyn(from) || isDyn(to)) return true;
|
|
308
|
+
// An unresolved parameter is an unknown type: it stands where anything is wanted and
|
|
309
|
+
// accepts anything, exactly as `dyn` does. What it does NOT have is members — asking
|
|
310
|
+
// for one has no answer, and the checker refuses there rather than here.
|
|
311
|
+
if (to.kind === "parameter" || from.kind === "parameter") return true;
|
|
312
|
+
if (from.kind === "union") return from.members.every((member) => assignable(member, to));
|
|
313
|
+
if (to.kind === "union") return to.members.some((member) => assignable(from, member));
|
|
314
|
+
if (from.kind === "nominal" || to.kind === "nominal") return typesEqual(from, to);
|
|
315
|
+
switch (to.kind) {
|
|
316
|
+
case "list":
|
|
317
|
+
return from.kind === "list" && assignable(from.element, to.element);
|
|
318
|
+
case "map":
|
|
319
|
+
return (
|
|
320
|
+
(from.kind === "map" && assignable(from.key, to.key) && assignable(from.value, to.value)) ||
|
|
321
|
+
// A record is a map with named keys, so it stands where a string-keyed map does.
|
|
322
|
+
(from.kind === "record" && assignable(STRING, to.key) && recordValuesAssignable(from, to.value))
|
|
323
|
+
);
|
|
324
|
+
case "optional":
|
|
325
|
+
return from.kind === "optional" && assignable(from.value, to.value);
|
|
326
|
+
case "record":
|
|
327
|
+
return from.kind === "record" && typesEqual(from, to);
|
|
328
|
+
default:
|
|
329
|
+
return typesEqual(from, to);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function recordValuesAssignable(record: RecordType, to: CelType): boolean {
|
|
334
|
+
if (isDyn(to)) return true;
|
|
335
|
+
if (record.open) return false;
|
|
336
|
+
return [...record.fields.values()].every((type) => assignable(type, to));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The one type both arms can be. Unlike assignability this is symmetric, and it is
|
|
341
|
+
* what an aggregate literal's elements and a ternary's branches are reduced through:
|
|
342
|
+
* nothing in common is `dyn`, which is CEL's own reading of a heterogeneous literal.
|
|
343
|
+
*
|
|
344
|
+
* **An unresolved parameter yields to the other side**, so the element type of
|
|
345
|
+
* `[[[[]]]], [], [[[]]]]` is the deepest list rather than `dyn`: an empty aggregate
|
|
346
|
+
* says nothing about its elements and must not erase what a sibling does say.
|
|
347
|
+
*/
|
|
348
|
+
export function unify(left: CelType, right: CelType): CelType {
|
|
349
|
+
if (typesEqual(left, right)) return left;
|
|
350
|
+
if (left.kind === "parameter") return right;
|
|
351
|
+
if (right.kind === "parameter") return left;
|
|
352
|
+
if (isDyn(left) || isDyn(right)) return DYN;
|
|
353
|
+
if (left.kind === "list" && right.kind === "list") return listOf(unify(left.element, right.element));
|
|
354
|
+
if (left.kind === "map" && right.kind === "map") {
|
|
355
|
+
return mapOf(unify(left.key, right.key), unify(left.value, right.value));
|
|
356
|
+
}
|
|
357
|
+
if (left.kind === "optional" && right.kind === "optional") return optionalOf(unify(left.value, right.value));
|
|
358
|
+
return DYN;
|
|
359
|
+
}
|