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