@telorun/cel 0.107.0 → 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/dist/backend-runtime.d.ts +46 -8
- package/dist/backend-runtime.d.ts.map +1 -1
- package/dist/backend-runtime.js +133 -30
- package/dist/catalog-runtime.d.ts.map +1 -1
- package/dist/catalog-runtime.js +110 -109
- package/dist/cel-map-value.d.ts +20 -7
- package/dist/cel-map-value.d.ts.map +1 -1
- package/dist/cel-map-value.js +29 -18
- package/dist/cel-value.d.ts +10 -3
- package/dist/cel-value.d.ts.map +1 -1
- package/dist/check-diagnostic.d.ts +3 -2
- package/dist/check-diagnostic.d.ts.map +1 -1
- package/dist/checker.d.ts +9 -0
- package/dist/checker.d.ts.map +1 -1
- package/dist/checker.js +98 -13
- package/dist/closure-backend.js +66 -15
- package/dist/duration-value.d.ts +11 -0
- package/dist/duration-value.d.ts.map +1 -1
- package/dist/duration-value.js +17 -3
- package/dist/emitted-module.d.ts +1 -1
- package/dist/emitted-module.js +1 -1
- package/dist/engine-version.d.ts +1 -1
- package/dist/engine-version.js +1 -1
- package/dist/environment.d.ts +8 -0
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +20 -5
- package/dist/function-registry.d.ts +40 -0
- package/dist/function-registry.d.ts.map +1 -1
- package/dist/function-registry.js +105 -2
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -6
- package/dist/js-emitter.d.ts +3 -2
- package/dist/js-emitter.d.ts.map +1 -1
- package/dist/js-emitter.js +11 -5
- package/dist/macro-check.d.ts +8 -0
- package/dist/macro-check.d.ts.map +1 -1
- package/dist/macro-check.js +1 -1
- package/dist/member-read.d.ts +42 -5
- package/dist/member-read.d.ts.map +1 -1
- package/dist/member-read.js +47 -6
- package/dist/resolved-call.d.ts +13 -0
- package/dist/resolved-call.d.ts.map +1 -1
- package/dist/runtime-library.d.ts +13 -2
- package/dist/runtime-library.d.ts.map +1 -1
- package/dist/runtime-library.js +134 -125
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +20 -4
- package/dist/timestamp-value.d.ts +9 -0
- package/dist/timestamp-value.d.ts.map +1 -1
- package/dist/timestamp-value.js +15 -0
- package/dist/type-expression.d.ts +18 -2
- package/dist/type-expression.d.ts.map +1 -1
- package/dist/type-expression.js +23 -3
- package/dist/value-equality.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/backend-runtime.ts +180 -30
- package/src/catalog-runtime.ts +133 -133
- package/src/cel-map-value.ts +31 -20
- package/src/cel-value.ts +11 -3
- package/src/check-diagnostic.ts +4 -2
- package/src/checker.ts +123 -10
- package/src/closure-backend.ts +59 -14
- package/src/duration-value.ts +18 -3
- package/src/emitted-module.ts +1 -1
- package/src/engine-version.ts +1 -1
- package/src/environment.ts +26 -5
- package/src/function-registry.ts +114 -2
- package/src/index.ts +24 -3
- package/src/js-emitter.ts +13 -5
- package/src/macro-check.ts +7 -1
- package/src/member-read.ts +48 -7
- package/src/resolved-call.ts +13 -0
- package/src/runtime-library.ts +167 -143
- package/src/serializer.ts +19 -3
- package/src/timestamp-value.ts +16 -0
- package/src/type-expression.ts +25 -3
- package/src/value-equality.ts +3 -3
package/src/cel-value.ts
CHANGED
|
@@ -82,12 +82,20 @@ export interface CelMapValueEntry {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
* What identifies a key inside a map's entries. A `Map` distinguishes its own keys **by
|
|
86
|
+
* type**, so the four CEL key types collapse onto three JS ones with no namespace to
|
|
87
|
+
* separate by hand: a string key is itself, a bool is itself, and an int, a uint and a
|
|
88
|
+
* whole double are all the `bigint` CEL equality makes them.
|
|
89
|
+
*/
|
|
90
|
+
export type CelMapKey = string | bigint | boolean;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A map. Entries live in a `Map` keyed by each key's own `CelMapKey`, so the map holds
|
|
94
|
+
* int, uint, bool and string keys alike and **no key is ever a property name**:
|
|
87
95
|
* `__proto__`, `constructor` and `prototype` are data here, as they are in any map.
|
|
88
96
|
*/
|
|
89
97
|
export interface CelMap extends Branded<"map"> {
|
|
90
|
-
readonly entries: ReadonlyMap<
|
|
98
|
+
readonly entries: ReadonlyMap<CelMapKey, CelMapValueEntry>;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
/** Every evaluation failure this engine names. A code is never derived from a message. */
|
package/src/check-diagnostic.ts
CHANGED
|
@@ -67,12 +67,14 @@ export interface CelCheckDiagnostic {
|
|
|
67
67
|
* refusal to compile a tree the front end already reported a syntax diagnostic for,
|
|
68
68
|
* which is a question the caller should have asked before compiling, and
|
|
69
69
|
* `emitted_module_rejected` is the refusal to run a loaded module whose integrity header
|
|
70
|
-
* is not the one the emission asked for
|
|
70
|
+
* is not the one the emission asked for, and `signature_too_wide` is a registration
|
|
71
|
+
* declaring more values than an implementation receives.
|
|
71
72
|
*/
|
|
72
73
|
export type CelEngineErrorCode =
|
|
73
74
|
| "namespaces_mismatch"
|
|
74
75
|
| "unreadable_expression"
|
|
75
|
-
| "emitted_module_rejected"
|
|
76
|
+
| "emitted_module_rejected"
|
|
77
|
+
| "signature_too_wide";
|
|
76
78
|
|
|
77
79
|
export class CelEngineError extends Error {
|
|
78
80
|
readonly code: CelEngineErrorCode;
|
package/src/checker.ts
CHANGED
|
@@ -80,6 +80,15 @@ export interface NamespaceFunction {
|
|
|
80
80
|
readonly parameters?: readonly CelType[];
|
|
81
81
|
/** How the declaration is written, for a listing; absent with the parameters. */
|
|
82
82
|
readonly signature?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Types the declaration named that nothing is registered under, in the order written.
|
|
85
|
+
*
|
|
86
|
+
* The declaration is accepted and reads `dyn` where one stands: a host's declaration is
|
|
87
|
+
* data out of someone's manifest, so a name its own registry does not carry is the host
|
|
88
|
+
* disagreeing with itself — reported at the call, where there is a range, rather than
|
|
89
|
+
* thrown at registration, where there is none. Same rule as an unjudged schema node.
|
|
90
|
+
*/
|
|
91
|
+
readonly unregisteredTypes?: readonly string[];
|
|
83
92
|
readonly deterministic?: boolean;
|
|
84
93
|
readonly hostBacked?: boolean;
|
|
85
94
|
readonly throws?: readonly string[];
|
|
@@ -171,6 +180,10 @@ class Checker implements MacroHost {
|
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
typeOfPresence(node: Extract<CelNode, { kind: "select" }>): CelType {
|
|
184
|
+
return this.selectType(node, true);
|
|
185
|
+
}
|
|
186
|
+
|
|
174
187
|
// --- the walk -----------------------------------------------------------
|
|
175
188
|
|
|
176
189
|
typeOf(node: CelNode): CelType {
|
|
@@ -186,7 +199,7 @@ class Checker implements MacroHost {
|
|
|
186
199
|
case "map":
|
|
187
200
|
return this.mapType(node);
|
|
188
201
|
case "select":
|
|
189
|
-
return this.selectType(node);
|
|
202
|
+
return this.selectType(node, node.optional);
|
|
190
203
|
case "index":
|
|
191
204
|
return this.indexType(node);
|
|
192
205
|
case "unary":
|
|
@@ -318,13 +331,19 @@ class Checker implements MacroHost {
|
|
|
318
331
|
return reduced;
|
|
319
332
|
}
|
|
320
333
|
|
|
321
|
-
|
|
334
|
+
/**
|
|
335
|
+
* `presence` is whether the read asks whether the member is THERE — written `.?b`, or
|
|
336
|
+
* standing as the argument of `has()`. It decides nothing about a type that is known to
|
|
337
|
+
* hold no members, which both forms refuse; it decides what a UNION's member-less
|
|
338
|
+
* branches are, which is the absence case a presence-shaped read answers for.
|
|
339
|
+
*/
|
|
340
|
+
private selectType(node: Extract<CelNode, { kind: "select" }>, presence: boolean): CelType {
|
|
322
341
|
const qualified = this.qualifiedVariableType(node);
|
|
323
342
|
if (qualified) return qualified;
|
|
324
343
|
const operand = this.typeOf(node.operand);
|
|
325
344
|
if (node.field === "") return DYN;
|
|
326
345
|
const guarded = this.checkNullable(node.operand, operand, `.${node.field}`, node.fieldRange);
|
|
327
|
-
const held = this.memberType(guarded, node.field, node.fieldRange);
|
|
346
|
+
const held = this.memberType(guarded, node.field, node.fieldRange, presence);
|
|
328
347
|
// Reading through an optional answers an optional, whichever form the read is
|
|
329
348
|
// written in: that is what makes a chain of reads over a value that may be absent
|
|
330
349
|
// stay one expression instead of needing a guard at every step.
|
|
@@ -367,11 +386,22 @@ class Checker implements MacroHost {
|
|
|
367
386
|
}
|
|
368
387
|
|
|
369
388
|
/** The type of a named member, reported against whatever the operand turned out to be. */
|
|
370
|
-
private memberType(
|
|
389
|
+
private memberType(
|
|
390
|
+
operand: CelType,
|
|
391
|
+
field: string,
|
|
392
|
+
range: SourceRange,
|
|
393
|
+
presence = false,
|
|
394
|
+
): CelType {
|
|
371
395
|
if (isDyn(operand) || operand.kind === "parameter") return DYN;
|
|
372
|
-
if (operand.kind === "optional")
|
|
396
|
+
if (operand.kind === "optional") {
|
|
397
|
+
return this.memberType(operand.value, field, range, presence);
|
|
398
|
+
}
|
|
373
399
|
if (operand.kind === "union") {
|
|
374
|
-
return unionOf(
|
|
400
|
+
return unionOf(
|
|
401
|
+
presenceBranches(operand, holdsMembers, presence).map((member) =>
|
|
402
|
+
this.memberType(member, field, range, presence),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
375
405
|
}
|
|
376
406
|
if (operand.kind === "record") return this.recordMemberType(operand, field, range);
|
|
377
407
|
if (operand.kind === "map") {
|
|
@@ -406,7 +436,7 @@ class Checker implements MacroHost {
|
|
|
406
436
|
const operand = this.typeOf(node.operand);
|
|
407
437
|
const index = this.typeOf(node.index);
|
|
408
438
|
const guarded = this.checkNullable(node.operand, operand, "[…]", node.range);
|
|
409
|
-
const held = this.elementType(guarded, index, node);
|
|
439
|
+
const held = this.elementType(guarded, index, node, node.optional);
|
|
410
440
|
return node.optional || guarded.kind === "optional" ? optionalOf(unwrapOptional(held)) : held;
|
|
411
441
|
}
|
|
412
442
|
|
|
@@ -414,11 +444,18 @@ class Checker implements MacroHost {
|
|
|
414
444
|
operand: CelType,
|
|
415
445
|
index: CelType,
|
|
416
446
|
node: Extract<CelNode, { kind: "index" }>,
|
|
447
|
+
presence = false,
|
|
417
448
|
): CelType {
|
|
418
449
|
if (isDyn(operand) || operand.kind === "parameter") return DYN;
|
|
419
|
-
if (operand.kind === "optional")
|
|
450
|
+
if (operand.kind === "optional") {
|
|
451
|
+
return this.elementType(operand.value, index, node, presence);
|
|
452
|
+
}
|
|
420
453
|
if (operand.kind === "union") {
|
|
421
|
-
return unionOf(
|
|
454
|
+
return unionOf(
|
|
455
|
+
presenceBranches(operand, holdsElements, presence).map((member) =>
|
|
456
|
+
this.elementType(member, index, node, presence),
|
|
457
|
+
),
|
|
458
|
+
);
|
|
422
459
|
}
|
|
423
460
|
if (operand.kind === "list") {
|
|
424
461
|
if (!assignable(index, INT) && !isDyn(index)) {
|
|
@@ -659,7 +696,7 @@ class Checker implements MacroHost {
|
|
|
659
696
|
if (failure.reason === "unknown") {
|
|
660
697
|
this.report(
|
|
661
698
|
"CEL_UNKNOWN_FUNCTION",
|
|
662
|
-
`no function named ${JSON.stringify(node.name)} is registered`,
|
|
699
|
+
`no function named ${JSON.stringify(node.name)} is registered${this.closestNames(node.name, form, args.length)}`,
|
|
663
700
|
node.range,
|
|
664
701
|
this.renameFix(node),
|
|
665
702
|
);
|
|
@@ -690,6 +727,28 @@ class Checker implements MacroHost {
|
|
|
690
727
|
);
|
|
691
728
|
}
|
|
692
729
|
|
|
730
|
+
/**
|
|
731
|
+
* What a call on an unregistered name is offered instead: the registered names that
|
|
732
|
+
* accept its form and arity, nearest first, as a clause to hang off the refusal.
|
|
733
|
+
*
|
|
734
|
+
* **The alternatives belong in the MESSAGE, and the fix stays singular.** A reader of
|
|
735
|
+
* `no('x')` is one name away from the five functions that would have worked, and the
|
|
736
|
+
* engine this one replaces said so; losing it is a real loss of help. A list of five
|
|
737
|
+
* repairs, though, is not a repair — an editor applying one would be guessing which
|
|
738
|
+
* function the author meant — so the single rename fix is still offered only where
|
|
739
|
+
* exactly one name is a case-insensitive match.
|
|
740
|
+
*
|
|
741
|
+
* It names no command and no tool: which listing a host offers is the host's, and an
|
|
742
|
+
* engine embedded in one with no CLI would be pointing at nothing.
|
|
743
|
+
*/
|
|
744
|
+
private closestNames(name: string, form: CallForm, arity: number): string {
|
|
745
|
+
const candidates = this.context.registry.candidateNames(name, form, arity);
|
|
746
|
+
if (candidates.length === 0) return "";
|
|
747
|
+
const written = arity === 0 ? "no arguments" : arity === 1 ? "1 argument" : `${arity} arguments`;
|
|
748
|
+
const shape = form === "receiver" ? `written on a value with ${written}` : `taking ${written}`;
|
|
749
|
+
return ` — the closest ${shape}: ${candidates.join(", ")}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
693
752
|
/** The one registered name this call might have meant, as a whole-source repair. */
|
|
694
753
|
private renameFix(node: CelCallNode | CelReceiverCallNode): CelDiagnosticFix | undefined {
|
|
695
754
|
const wanted = node.name.toLowerCase();
|
|
@@ -757,6 +816,7 @@ class Checker implements MacroHost {
|
|
|
757
816
|
namespace: node.namespace,
|
|
758
817
|
arity: args.length,
|
|
759
818
|
range: node.range,
|
|
819
|
+
argumentTypes: args.map((type) => formatType(withoutParameters(type))),
|
|
760
820
|
});
|
|
761
821
|
// An OPEN namespace declares only part of what it reaches, so a name it does not
|
|
762
822
|
// carry is not this engine's to refuse — it is listed and left to the host.
|
|
@@ -775,12 +835,27 @@ class Checker implements MacroHost {
|
|
|
775
835
|
namespace: node.namespace,
|
|
776
836
|
arity: args.length,
|
|
777
837
|
range: node.range,
|
|
838
|
+
argumentTypes: args.map((type) => formatType(withoutParameters(type))),
|
|
778
839
|
...(declared.signature === undefined ? {} : { signature: declared.signature }),
|
|
779
840
|
returns: formatType(withoutParameters(declared.returns)),
|
|
780
841
|
deterministic: declared.deterministic ?? true,
|
|
781
842
|
hostBacked: declared.hostBacked ?? false,
|
|
782
843
|
...(declared.throws ? { throws: declared.throws } : {}),
|
|
783
844
|
});
|
|
845
|
+
// A type the host named and its own registry does not carry: the declaration stands,
|
|
846
|
+
// reads `dyn` where that type stood, and the disagreement is a verdict HERE — this is
|
|
847
|
+
// the only place the name has a range.
|
|
848
|
+
const unregistered = declared.unregisteredTypes ?? [];
|
|
849
|
+
if (unregistered.length > 0) {
|
|
850
|
+
const names = unregistered.map((name) => JSON.stringify(name)).join(", ");
|
|
851
|
+
this.report(
|
|
852
|
+
"CEL_TYPE_ERROR",
|
|
853
|
+
unregistered.length === 1
|
|
854
|
+
? `${qualified} is declared over the type ${names}, and no type is registered under that name`
|
|
855
|
+
: `${qualified} is declared over the types ${names}, and no type is registered under those names`,
|
|
856
|
+
node.range,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
784
859
|
// Parameters withheld: the host judges arity and arguments against its own, richer
|
|
785
860
|
// signature grammar, and this engine types the result and says nothing else.
|
|
786
861
|
if (parameters === undefined) return declared.returns;
|
|
@@ -815,6 +890,44 @@ function isNullTest(args: readonly CelType[]): boolean {
|
|
|
815
890
|
return (isNull(left) && admitsNull(right)) || (isNull(right) && admitsNull(left));
|
|
816
891
|
}
|
|
817
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Which branches of a union a read is judged against.
|
|
895
|
+
*
|
|
896
|
+
* A **presence-shaped** read — `a.?b`, `a[?k]`, `has(a.b)` — asks whether the member is
|
|
897
|
+
* THERE, so a branch that can hold no member at all is the absence case it answers
|
|
898
|
+
* `optional.none()` / `false` for at runtime, not a mistake in the expression: that is how
|
|
899
|
+
* a two-shape field is discriminated without a type test. Where **no** branch can hold one
|
|
900
|
+
* the read is judged against them all, so an operand that is known to hold no members is
|
|
901
|
+
* still refused in the presence form exactly as in the plain one.
|
|
902
|
+
*/
|
|
903
|
+
function presenceBranches(
|
|
904
|
+
union: Extract<CelType, { kind: "union" }>,
|
|
905
|
+
holds: (type: CelType) => boolean,
|
|
906
|
+
presence: boolean,
|
|
907
|
+
): readonly CelType[] {
|
|
908
|
+
if (!presence) return union.members;
|
|
909
|
+
const holding = union.members.filter(holds);
|
|
910
|
+
return holding.length > 0 ? holding : union.members;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** Whether a type can hold a named member at all — what `memberType` reads one of. */
|
|
914
|
+
function holdsMembers(type: CelType): boolean {
|
|
915
|
+
if (isDyn(type) || type.kind === "parameter") return true;
|
|
916
|
+
if (type.kind === "record" || type.kind === "map") return true;
|
|
917
|
+
if (type.kind === "optional") return holdsMembers(type.value);
|
|
918
|
+
if (type.kind === "union") return type.members.some(holdsMembers);
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** Whether a type can hold an element at all — what `elementType` reads one of. */
|
|
923
|
+
function holdsElements(type: CelType): boolean {
|
|
924
|
+
if (isDyn(type) || type.kind === "parameter") return true;
|
|
925
|
+
if (type.kind === "record" || type.kind === "map" || type.kind === "list") return true;
|
|
926
|
+
if (type.kind === "optional") return holdsElements(type.value);
|
|
927
|
+
if (type.kind === "union") return type.members.some(holdsElements);
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
|
|
818
931
|
/** An optional of an optional is one optional; a chain of reads does not nest them. */
|
|
819
932
|
function unwrapOptional(type: CelType): CelType {
|
|
820
933
|
return type.kind === "optional" ? unwrapOptional(type.value) : type;
|
package/src/closure-backend.ts
CHANGED
|
@@ -186,21 +186,23 @@ class Compiler {
|
|
|
186
186
|
}));
|
|
187
187
|
const { range } = node;
|
|
188
188
|
return (frame) => {
|
|
189
|
-
|
|
189
|
+
// Flat — key, value, key, value — so a literal of n entries costs one allocation
|
|
190
|
+
// rather than one per entry. The emitter writes the same call.
|
|
191
|
+
const flat: CelValue[] = [];
|
|
190
192
|
for (const entry of entries) {
|
|
191
193
|
const key = entry.key(frame);
|
|
192
194
|
if (isCelError(key)) return key;
|
|
193
195
|
const value = entry.value(frame);
|
|
194
196
|
if (isCelError(value)) return value;
|
|
195
197
|
if (!entry.optional) {
|
|
196
|
-
|
|
198
|
+
flat.push(key, value);
|
|
197
199
|
continue;
|
|
198
200
|
}
|
|
199
201
|
const held = optionalEntry(value, range);
|
|
200
202
|
if (isCelError(held)) return held;
|
|
201
|
-
if (held.present)
|
|
203
|
+
if (held.present) flat.push(key, held.held as CelValue);
|
|
202
204
|
}
|
|
203
|
-
return celMapFromEntries(
|
|
205
|
+
return celMapFromEntries(flat, range);
|
|
204
206
|
};
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -297,20 +299,63 @@ class Compiler {
|
|
|
297
299
|
|
|
298
300
|
/**
|
|
299
301
|
* One dispatch: evaluate the arguments, carry the first error out, then hand the values
|
|
300
|
-
* to the site
|
|
302
|
+
* to the site **positionally**, which resolves the overload on their own types. The arity
|
|
303
|
+
* is the dispatch key's and known at compile time, so the step is chosen once here and no
|
|
304
|
+
* argument array is built per call — the same choice the emitter writes into its text.
|
|
301
305
|
*/
|
|
302
306
|
private callStep(name: string, form: CallForm, args: readonly CelStep[], range: SourceRange): CelStep {
|
|
303
307
|
const site = callSiteOf(this.target, name, form, range);
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
const [first, second, third, fourth] = args;
|
|
309
|
+
switch (args.length) {
|
|
310
|
+
case 0:
|
|
311
|
+
return () => site.call0();
|
|
312
|
+
case 1:
|
|
313
|
+
return (frame) => {
|
|
314
|
+
const a = first!(frame);
|
|
315
|
+
return isCelError(a) ? a : site.call1(a);
|
|
316
|
+
};
|
|
317
|
+
case 2:
|
|
318
|
+
return (frame) => {
|
|
319
|
+
const a = first!(frame);
|
|
320
|
+
if (isCelError(a)) return a;
|
|
321
|
+
const b = second!(frame);
|
|
322
|
+
return isCelError(b) ? b : site.call2(a, b);
|
|
323
|
+
};
|
|
324
|
+
case 3:
|
|
325
|
+
return (frame) => {
|
|
326
|
+
const a = first!(frame);
|
|
327
|
+
if (isCelError(a)) return a;
|
|
328
|
+
const b = second!(frame);
|
|
329
|
+
if (isCelError(b)) return b;
|
|
330
|
+
const c = third!(frame);
|
|
331
|
+
return isCelError(c) ? c : site.call3(a, b, c);
|
|
332
|
+
};
|
|
333
|
+
case 4:
|
|
334
|
+
return (frame) => {
|
|
335
|
+
const a = first!(frame);
|
|
336
|
+
if (isCelError(a)) return a;
|
|
337
|
+
const b = second!(frame);
|
|
338
|
+
if (isCelError(b)) return b;
|
|
339
|
+
const c = third!(frame);
|
|
340
|
+
if (isCelError(c)) return c;
|
|
341
|
+
const d = fourth!(frame);
|
|
342
|
+
return isCelError(d) ? d : site.call4(a, b, c, d);
|
|
343
|
+
};
|
|
344
|
+
default: {
|
|
345
|
+
// A call written wider than any signature may be. Its arguments are still evaluated
|
|
346
|
+
// in order, so an error in one carries out ahead of the refusal.
|
|
347
|
+
const count = args.length;
|
|
348
|
+
return (frame) => {
|
|
349
|
+
const values: CelValue[] = new Array<CelValue>(count);
|
|
350
|
+
for (let at = 0; at < count; at += 1) {
|
|
351
|
+
const held = args[at]!(frame);
|
|
352
|
+
if (isCelError(held)) return held;
|
|
353
|
+
values[at] = held;
|
|
354
|
+
}
|
|
355
|
+
return site.call(values);
|
|
356
|
+
};
|
|
311
357
|
}
|
|
312
|
-
|
|
313
|
-
};
|
|
358
|
+
}
|
|
314
359
|
}
|
|
315
360
|
|
|
316
361
|
private qualifiedCallStep(
|
package/src/duration-value.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* own spelling, and the one plain encoding a duration has wherever it is written down.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { celError, CEL_VALUE_TYPE, type CelDuration, type CelError } from "./cel-value.js";
|
|
26
|
+
import { celError, CEL_VALUE_TYPE, isCelError, type CelDuration, type CelError } from "./cel-value.js";
|
|
27
27
|
import type { SourceRange } from "./syntax-tree.js";
|
|
28
28
|
|
|
29
29
|
/** The widest and narrowest total a duration holds, in nanoseconds: int64. */
|
|
@@ -78,6 +78,21 @@ const PART = /([0-9]*)(?:\.([0-9]*))?(ns|us|µs|μs|ms|s|m|h)/g;
|
|
|
78
78
|
* `ns`, `us`, `ms`, `s`, `m` and `h` (`1h30m`, `1.5s`, `-10m`).
|
|
79
79
|
*/
|
|
80
80
|
export function parseDuration(text: string, range?: SourceRange): CelDuration | CelError {
|
|
81
|
+
const total = durationNanosFromText(text, range);
|
|
82
|
+
return isCelError(total) ? total : celDurationFromNanos(total, range);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The same text as a TOTAL OF NANOSECONDS, with no range applied.
|
|
87
|
+
*
|
|
88
|
+
* The grammar and the range are separate questions, and a consumer outside CEL has the same
|
|
89
|
+
* grammar with a different range: protobuf's `google.protobuf.Duration` reaches ±10,000
|
|
90
|
+
* years, which **cannot be held in an int64 of nanoseconds at all**, so a reader that must
|
|
91
|
+
* carry one (a journal entry, a value off a transport) cannot go through `parseDuration` and
|
|
92
|
+
* would otherwise restate this grammar. It answers an unbounded `bigint`; applying a range
|
|
93
|
+
* is the caller's, and `celDurationFromNanos` is what applies CEL's.
|
|
94
|
+
*/
|
|
95
|
+
export function durationNanosFromText(text: string, range?: SourceRange): bigint | CelError {
|
|
81
96
|
const refuse = () =>
|
|
82
97
|
celError("invalid_conversion", `${JSON.stringify(text)} is not a duration`, range);
|
|
83
98
|
let body = text;
|
|
@@ -86,7 +101,7 @@ export function parseDuration(text: string, range?: SourceRange): CelDuration |
|
|
|
86
101
|
negative = body.startsWith("-");
|
|
87
102
|
body = body.slice(1);
|
|
88
103
|
}
|
|
89
|
-
if (body === "0") return
|
|
104
|
+
if (body === "0") return 0n;
|
|
90
105
|
if (body === "") return refuse();
|
|
91
106
|
PART.lastIndex = 0;
|
|
92
107
|
let total = 0n;
|
|
@@ -104,7 +119,7 @@ export function parseDuration(text: string, range?: SourceRange): CelDuration |
|
|
|
104
119
|
at += whole.length;
|
|
105
120
|
}
|
|
106
121
|
if (at !== body.length) return refuse();
|
|
107
|
-
return
|
|
122
|
+
return negative ? -total : total;
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
/** Seconds with an `s` suffix, the fraction trimmed to what it carries. */
|
package/src/emitted-module.ts
CHANGED
|
@@ -94,7 +94,7 @@ import type { CelNode } from "./syntax-tree.js";
|
|
|
94
94
|
* code the emitter writes for a corpus drawn from the package's own total enumerations, beside
|
|
95
95
|
* this number, so a change to that text fails naming the bump it owes.
|
|
96
96
|
*/
|
|
97
|
-
export const EMITTER_FORMAT_GENERATION =
|
|
97
|
+
export const EMITTER_FORMAT_GENERATION = 2;
|
|
98
98
|
|
|
99
99
|
/** The prefix of the one line a stored module's header is read from. */
|
|
100
100
|
const HEADER_PREFIX = "//@telo.cel ";
|
package/src/engine-version.ts
CHANGED
package/src/environment.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { environmentDigest } from "./environment-digest.js";
|
|
|
37
37
|
import type { CelValue } from "./cel-value.js";
|
|
38
38
|
import { CEL_VALUE_KEYS } from "./cel-value.js";
|
|
39
39
|
import type { CelType } from "./cel-type.js";
|
|
40
|
-
import { formatType } from "./cel-type.js";
|
|
40
|
+
import { DYN, formatType } from "./cel-type.js";
|
|
41
41
|
import { CelEngineError } from "./check-diagnostic.js";
|
|
42
42
|
import type { CheckResult, NamespaceFunction } from "./checker.js";
|
|
43
43
|
import { checkExpression } from "./checker.js";
|
|
@@ -182,6 +182,14 @@ export interface SchemaRegistrationReport {
|
|
|
182
182
|
* by construction rather than by a flag: a declaration that carried parameters and asked for
|
|
183
183
|
* them to be ignored would hold a list nothing reads, which no reader can tell from a list
|
|
184
184
|
* that is simply wrong.
|
|
185
|
+
*
|
|
186
|
+
* **A type name this environment registers nothing under is accepted and reads `dyn`**, and
|
|
187
|
+
* every call to such a function carries a ranged `CEL_TYPE_ERROR` naming it. A declaration
|
|
188
|
+
* is a host's own data — a module function's declared result out of a manifest — so a typo
|
|
189
|
+
* in one is the host disagreeing with its own registry, which is reported where there is a
|
|
190
|
+
* range rather than thrown where there is none. A type registered AFTER a declaration naming
|
|
191
|
+
* it does not change that declaration: a declaration resolves its types once, when it is
|
|
192
|
+
* made.
|
|
185
193
|
*/
|
|
186
194
|
export type NamespaceFunctionDeclaration = (
|
|
187
195
|
| { readonly signature: string; readonly name?: never; readonly returns?: never }
|
|
@@ -443,8 +451,21 @@ export class CelEnvironment {
|
|
|
443
451
|
...(entry.hostBacked === undefined ? {} : { hostBacked: entry.hostBacked }),
|
|
444
452
|
...(entry.throws === undefined ? {} : { throws: entry.throws }),
|
|
445
453
|
};
|
|
454
|
+
// A name this environment registers no type under is RECORDED and read as `dyn`,
|
|
455
|
+
// never thrown: a namespace declaration is a host's own data — a module function's
|
|
456
|
+
// declared result out of a manifest — so a typo there would otherwise be a crash
|
|
457
|
+
// with no line, and the consumer that knows where it was written is the one that
|
|
458
|
+
// can anchor the diagnostic. The checker reports it at each call.
|
|
459
|
+
const unregistered: string[] = [];
|
|
460
|
+
const resolver: NominalResolver = (named, args) => {
|
|
461
|
+
const resolved = this.nominalResolver(named, args);
|
|
462
|
+
if (resolved) return resolved;
|
|
463
|
+
if (!unregistered.includes(named)) unregistered.push(named);
|
|
464
|
+
return DYN;
|
|
465
|
+
};
|
|
466
|
+
const recorded = () => (unregistered.length > 0 ? { unregisteredTypes: [...unregistered] } : {});
|
|
446
467
|
if (entry.signature !== undefined) {
|
|
447
|
-
const signature = parseSignature(entry.signature,
|
|
468
|
+
const signature = parseSignature(entry.signature, resolver);
|
|
448
469
|
if (signature.form !== "global") {
|
|
449
470
|
throw new CelTypeRegistrationError(
|
|
450
471
|
`a namespaced function is declared without a receiver: ${JSON.stringify(entry.signature)}`,
|
|
@@ -455,6 +476,7 @@ export class CelEnvironment {
|
|
|
455
476
|
returns: signature.returns,
|
|
456
477
|
parameters: signature.parameters,
|
|
457
478
|
signature: formatSignature(signature),
|
|
479
|
+
...recorded(),
|
|
458
480
|
...flags,
|
|
459
481
|
});
|
|
460
482
|
continue;
|
|
@@ -462,9 +484,8 @@ export class CelEnvironment {
|
|
|
462
484
|
declared.set(entry.name, {
|
|
463
485
|
name: entry.name,
|
|
464
486
|
returns:
|
|
465
|
-
typeof entry.returns === "string"
|
|
466
|
-
|
|
467
|
-
: entry.returns,
|
|
487
|
+
typeof entry.returns === "string" ? parseTypeExpression(entry.returns, resolver) : entry.returns,
|
|
488
|
+
...recorded(),
|
|
468
489
|
...flags,
|
|
469
490
|
});
|
|
470
491
|
}
|
package/src/function-registry.ts
CHANGED
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* once, with the candidates it considered.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { CelEngineError } from "./check-diagnostic.js";
|
|
18
19
|
import type { CelType } from "./cel-type.js";
|
|
19
|
-
import { assignable, DYN, formatType, isDyn } from "./cel-type.js";
|
|
20
|
+
import { assignable, DYN, formatType, isDyn, typesEqual } from "./cel-type.js";
|
|
20
21
|
import type { CallForm, CelSignature, FunctionMetadata } from "./signature.js";
|
|
21
22
|
import { formatSignature, signatureKey } from "./signature.js";
|
|
23
|
+
import { CALL_SITE_DIRECT_ARITY } from "./runtime-library.js";
|
|
24
|
+
import { isIdentifierSpelling, isReservedWord } from "./reserved-words.js";
|
|
22
25
|
|
|
23
26
|
export interface RegisteredFunction {
|
|
24
27
|
readonly signature: CelSignature;
|
|
@@ -33,6 +36,16 @@ export type ResolutionFailure =
|
|
|
33
36
|
/** The name and form are registered; no overload takes these arguments. */
|
|
34
37
|
| { readonly reason: "no-overload"; readonly candidates: readonly RegisteredFunction[] };
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* How many names a refused call may name as candidates.
|
|
41
|
+
*
|
|
42
|
+
* It is **declared** rather than left to a message's taste, because the conformance
|
|
43
|
+
* vectors pin that message byte for byte: a bound and an order a second engine cannot
|
|
44
|
+
* reproduce would be a row no port can pass. The order is edit distance then name, both
|
|
45
|
+
* over the name exactly as it was written.
|
|
46
|
+
*/
|
|
47
|
+
export const UNKNOWN_FUNCTION_CANDIDATES = 5;
|
|
48
|
+
|
|
36
49
|
export interface Resolution {
|
|
37
50
|
readonly resolved: RegisteredFunction;
|
|
38
51
|
/** The return type with every type parameter substituted by what this call bound. */
|
|
@@ -51,6 +64,16 @@ export class FunctionRegistry {
|
|
|
51
64
|
|
|
52
65
|
/** Registers a function, replacing any registration answering the same call. */
|
|
53
66
|
register(signature: CelSignature, metadata: FunctionMetadata = {}): void {
|
|
67
|
+
// An implementation receives its arguments positionally, up to the bound, so a wider
|
|
68
|
+
// signature is refused HERE rather than resolved and then called with its tail dropped.
|
|
69
|
+
// A declared bound nothing enforces is the silent wrong answer it exists to prevent.
|
|
70
|
+
const arity = signature.parameters.length + (signature.form === "receiver" ? 1 : 0);
|
|
71
|
+
if (arity > CALL_SITE_DIRECT_ARITY) {
|
|
72
|
+
throw new CelEngineError(
|
|
73
|
+
"signature_too_wide",
|
|
74
|
+
`${formatSignature(signature)} takes ${arity} values and an implementation receives at most ${CALL_SITE_DIRECT_ARITY}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
54
77
|
const entries = this.byName.get(signature.name) ?? [];
|
|
55
78
|
const key = signatureKey(signature);
|
|
56
79
|
const at = entries.findIndex((entry) => signatureKey(entry.signature) === key);
|
|
@@ -93,6 +116,38 @@ export class FunctionRegistry {
|
|
|
93
116
|
return this.byName.get(name) ?? [];
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The names this environment registers that would accept a call of that form and
|
|
121
|
+
* arity, nearest first — what a call on a name nothing registers is offered instead.
|
|
122
|
+
*
|
|
123
|
+
* **Form and arity are a filter rather than a ranking**, because a nearer name a call
|
|
124
|
+
* cannot be written on is not a repair: `'a'.size()` is not helped by `size`. A name
|
|
125
|
+
* that is not a name is filtered for the same reason — the operators register under
|
|
126
|
+
* their own symbols (`!`, `-`, `+`), and `no(1)` offered `!` and `-` as its two
|
|
127
|
+
* nearest spellings, neither of which can be written as a call at all. Among what
|
|
128
|
+
* passes the filter the order is edit distance then name, and the list is cut at
|
|
129
|
+
* {@link UNKNOWN_FUNCTION_CANDIDATES} — a declared bound and a declared order, so the
|
|
130
|
+
* message is the same text on every engine. Distance is counted over the name as
|
|
131
|
+
* written, with no case folding: a port reproduces one rule and not a host language's
|
|
132
|
+
* notion of lower case.
|
|
133
|
+
*/
|
|
134
|
+
candidateNames(wanted: string, form: CallForm, arity: number): readonly string[] {
|
|
135
|
+
const matching: string[] = [];
|
|
136
|
+
for (const [name, entries] of this.byName) {
|
|
137
|
+
if (name === wanted) continue;
|
|
138
|
+
if (!isIdentifierSpelling(name) || isReservedWord(name)) continue;
|
|
139
|
+
const accepts = entries.some(
|
|
140
|
+
(entry) => entry.signature.form === form && entry.signature.parameters.length === arity,
|
|
141
|
+
);
|
|
142
|
+
if (accepts) matching.push(name);
|
|
143
|
+
}
|
|
144
|
+
return matching
|
|
145
|
+
.map((name) => ({ name, distance: editDistance(wanted, name) }))
|
|
146
|
+
.sort((left, right) => left.distance - right.distance || compareNames(left.name, right.name))
|
|
147
|
+
.slice(0, UNKNOWN_FUNCTION_CANDIDATES)
|
|
148
|
+
.map((candidate) => candidate.name);
|
|
149
|
+
}
|
|
150
|
+
|
|
96
151
|
/**
|
|
97
152
|
* The function a call resolves to. `receiver` is the type the call is written on,
|
|
98
153
|
* absent for a global call.
|
|
@@ -114,11 +169,41 @@ export class FunctionRegistry {
|
|
|
114
169
|
// unlisted variable from turning one mistake into two.
|
|
115
170
|
const exact = this.match(viable, args, receiver, false);
|
|
116
171
|
if (exact) return exact;
|
|
117
|
-
const loose = this.
|
|
172
|
+
const loose = this.matchLoosely(viable, args, receiver);
|
|
118
173
|
if (loose) return loose;
|
|
119
174
|
return { reason: "no-overload", candidates: inForm };
|
|
120
175
|
}
|
|
121
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The loose pass: a `dyn` argument makes a candidate viable, and **where several become
|
|
179
|
+
* viable and they do not agree on a return type, the call answers `dyn`.**
|
|
180
|
+
*
|
|
181
|
+
* Taking the first one instead is a concrete type nothing established. `dyn('a') +
|
|
182
|
+
* dyn('b')` answered `int` — the first `+` overload — so a host that declares nothing
|
|
183
|
+
* about a name had `a + b` typed `int`, and a consumer comparing that against a declared
|
|
184
|
+
* `string` slot refused a manifest that runs correctly. The honest answer is the one the
|
|
185
|
+
* engine's own design already states: overloads are resolved per call site on the VALUES'
|
|
186
|
+
* own types, so a `dyn` operand is exactly the case where the static answer is unknown.
|
|
187
|
+
*
|
|
188
|
+
* The candidate is still carried for the caller that wants one (a listing, a flag), and a
|
|
189
|
+
* single viable overload still answers its own return type — `dyn` only where they differ.
|
|
190
|
+
*/
|
|
191
|
+
private matchLoosely(
|
|
192
|
+
candidates: readonly RegisteredFunction[],
|
|
193
|
+
args: readonly CelType[],
|
|
194
|
+
receiver: CelType | undefined,
|
|
195
|
+
): Resolution | undefined {
|
|
196
|
+
const viable: Resolution[] = [];
|
|
197
|
+
for (const candidate of candidates) {
|
|
198
|
+
const held = this.match([candidate], args, receiver, true);
|
|
199
|
+
if (held) viable.push(held);
|
|
200
|
+
}
|
|
201
|
+
const first = viable[0];
|
|
202
|
+
if (!first) return undefined;
|
|
203
|
+
const agree = viable.every((held) => typesEqual(held.returns, first.returns));
|
|
204
|
+
return agree ? first : { resolved: first.resolved, returns: DYN };
|
|
205
|
+
}
|
|
206
|
+
|
|
122
207
|
private match(
|
|
123
208
|
candidates: readonly RegisteredFunction[],
|
|
124
209
|
args: readonly CelType[],
|
|
@@ -219,6 +304,33 @@ export function substitute(type: CelType, bindings: ReadonlyMap<string, CelType>
|
|
|
219
304
|
}
|
|
220
305
|
}
|
|
221
306
|
|
|
307
|
+
/** Code-unit order, so a port orders two names without a locale. */
|
|
308
|
+
function compareNames(left: string, right: string): number {
|
|
309
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Levenshtein distance in UTF-16 code units — one insertion, deletion or substitution
|
|
314
|
+
* each costing one, which is the whole rule a port needs to reproduce.
|
|
315
|
+
*
|
|
316
|
+
* Code units rather than code points for the same reason every range in this package is
|
|
317
|
+
* in them: it is the one unit both ends of the engine already count in, and a name is
|
|
318
|
+
* compared against a name rather than cut.
|
|
319
|
+
*/
|
|
320
|
+
function editDistance(from: string, to: string): number {
|
|
321
|
+
let previous = Array.from({ length: to.length + 1 }, (ignored, at) => at);
|
|
322
|
+
for (let left = 1; left <= from.length; left += 1) {
|
|
323
|
+
const row = new Array<number>(to.length + 1);
|
|
324
|
+
row[0] = left;
|
|
325
|
+
for (let right = 1; right <= to.length; right += 1) {
|
|
326
|
+
const substitution = previous[right - 1]! + (from[left - 1] === to[right - 1] ? 0 : 1);
|
|
327
|
+
row[right] = Math.min(substitution, previous[right]! + 1, row[right - 1]! + 1);
|
|
328
|
+
}
|
|
329
|
+
previous = row;
|
|
330
|
+
}
|
|
331
|
+
return previous[to.length]!;
|
|
332
|
+
}
|
|
333
|
+
|
|
222
334
|
/** How a resolution failure names what it looked at, for a message a human reads. */
|
|
223
335
|
export function describeCandidates(candidates: readonly RegisteredFunction[]): string {
|
|
224
336
|
return candidates.map((candidate) => formatSignature(candidate.signature)).join(", ");
|