@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types as text: `list<int>`, `map<string, int>`, `optional<T>`, `Money<string>`.
|
|
3
|
+
*
|
|
4
|
+
* A host declares a variable's or a signature's type as a string, and the signature
|
|
5
|
+
* data a port reads is text, so one grammar reads both. It is deliberately the same
|
|
6
|
+
* spelling `formatType` writes, so a type that round-trips through text is the type
|
|
7
|
+
* it started as — which is what lets the definition listing and the signature data
|
|
8
|
+
* be compared without a second parser.
|
|
9
|
+
*
|
|
10
|
+
* A bare capital letter is a type **parameter**, which is how a signature says "the
|
|
11
|
+
* same type here and there". A name the host registered as a nominal type resolves
|
|
12
|
+
* to it; any other name is refused rather than guessed — as a
|
|
13
|
+
* {@link CelUnknownTypeNameError}, which a caller holding the name's source may turn
|
|
14
|
+
* into a verdict — because a misspelled type in a signature would otherwise register a
|
|
15
|
+
* function nothing can call.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { CelType, PrimitiveName } from "./cel-type.js";
|
|
19
|
+
import {
|
|
20
|
+
BOOL,
|
|
21
|
+
BYTES,
|
|
22
|
+
DOUBLE,
|
|
23
|
+
DYN,
|
|
24
|
+
DURATION,
|
|
25
|
+
INT,
|
|
26
|
+
listOf,
|
|
27
|
+
mapOf,
|
|
28
|
+
NULL,
|
|
29
|
+
optionalOf,
|
|
30
|
+
parameterOf,
|
|
31
|
+
STRING,
|
|
32
|
+
TIMESTAMP,
|
|
33
|
+
TYPE,
|
|
34
|
+
UINT,
|
|
35
|
+
WELL_KNOWN_TYPE_NAMES,
|
|
36
|
+
} from "./cel-type.js";
|
|
37
|
+
|
|
38
|
+
export class CelTypeExpressionError extends Error {
|
|
39
|
+
constructor(message: string) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "CelTypeExpressionError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A well-formed type expression naming a type nothing is registered under.
|
|
47
|
+
*
|
|
48
|
+
* It is **not** a `CelTypeExpressionError`, and the split is the whole point: that error
|
|
49
|
+
* is about text this grammar cannot read, while this one is about a name the HOST and its
|
|
50
|
+
* own registry disagree about. A caller that holds where the name was written can turn it
|
|
51
|
+
* into a ranged verdict instead of a crash — which is what a namespace declaration built
|
|
52
|
+
* out of a host's data does — and a caller that cannot still lets it fly.
|
|
53
|
+
*/
|
|
54
|
+
export class CelUnknownTypeNameError extends Error {
|
|
55
|
+
/** The name nothing is registered under. */
|
|
56
|
+
readonly typeName: string;
|
|
57
|
+
|
|
58
|
+
constructor(typeName: string) {
|
|
59
|
+
super(`no type is registered under the name ${JSON.stringify(typeName)}`);
|
|
60
|
+
this.name = "CelUnknownTypeNameError";
|
|
61
|
+
this.typeName = typeName;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const PRIMITIVES: Readonly<Record<string, CelType>> = {
|
|
66
|
+
dyn: DYN,
|
|
67
|
+
int: INT,
|
|
68
|
+
uint: UINT,
|
|
69
|
+
double: DOUBLE,
|
|
70
|
+
bool: BOOL,
|
|
71
|
+
string: STRING,
|
|
72
|
+
bytes: BYTES,
|
|
73
|
+
null: NULL,
|
|
74
|
+
null_type: NULL,
|
|
75
|
+
type: TYPE,
|
|
76
|
+
timestamp: TIMESTAMP,
|
|
77
|
+
duration: DURATION,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const PARAMETER = /^[A-Z]$/;
|
|
81
|
+
|
|
82
|
+
/** What a name that is neither a primitive nor a parameter resolves to. */
|
|
83
|
+
export type NominalResolver = (name: string, args: readonly CelType[]) => CelType | undefined;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Reads one type expression. `resolveNominal` is consulted for every name the
|
|
87
|
+
* language itself does not define.
|
|
88
|
+
*/
|
|
89
|
+
export function parseTypeExpression(text: string, resolveNominal?: NominalResolver): CelType {
|
|
90
|
+
const reader = new TypeReader(text, resolveNominal);
|
|
91
|
+
const type = reader.read();
|
|
92
|
+
reader.expectEnd();
|
|
93
|
+
return type;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class TypeReader {
|
|
97
|
+
private at = 0;
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
private readonly text: string,
|
|
101
|
+
private readonly resolveNominal?: NominalResolver,
|
|
102
|
+
) {}
|
|
103
|
+
|
|
104
|
+
read(): CelType {
|
|
105
|
+
const name = this.readName();
|
|
106
|
+
const args = this.readArguments();
|
|
107
|
+
return this.resolve(name, args);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
expectEnd(): void {
|
|
111
|
+
this.skipSpace();
|
|
112
|
+
if (this.at !== this.text.length) {
|
|
113
|
+
throw new CelTypeExpressionError(`${JSON.stringify(this.text)} is not one type expression`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Reads the next type of a list, stopping at `,` or `>`. */
|
|
118
|
+
readArgument(): CelType {
|
|
119
|
+
return this.read();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private skipSpace(): void {
|
|
123
|
+
while (this.text[this.at] === " ") this.at += 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private readName(): string {
|
|
127
|
+
this.skipSpace();
|
|
128
|
+
const from = this.at;
|
|
129
|
+
while (this.at < this.text.length && /[A-Za-z0-9_.]/.test(this.text[this.at]!)) this.at += 1;
|
|
130
|
+
if (this.at === from) {
|
|
131
|
+
throw new CelTypeExpressionError(`${JSON.stringify(this.text)} names no type at offset ${from}`);
|
|
132
|
+
}
|
|
133
|
+
return this.text.slice(from, this.at);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private readArguments(): CelType[] {
|
|
137
|
+
this.skipSpace();
|
|
138
|
+
if (this.text[this.at] !== "<") return [];
|
|
139
|
+
this.at += 1;
|
|
140
|
+
const args: CelType[] = [this.readArgument()];
|
|
141
|
+
for (;;) {
|
|
142
|
+
this.skipSpace();
|
|
143
|
+
const next = this.text[this.at];
|
|
144
|
+
if (next === ",") {
|
|
145
|
+
this.at += 1;
|
|
146
|
+
args.push(this.readArgument());
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (next === ">") {
|
|
150
|
+
this.at += 1;
|
|
151
|
+
return args;
|
|
152
|
+
}
|
|
153
|
+
throw new CelTypeExpressionError(
|
|
154
|
+
`${JSON.stringify(this.text)} has no closing '>' for its type arguments`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private resolve(name: string, args: readonly CelType[]): CelType {
|
|
160
|
+
const wellKnown = WELL_KNOWN_TYPE_NAMES[name];
|
|
161
|
+
if (wellKnown) return this.noArguments(name, args, PRIMITIVES[wellKnown as PrimitiveName]!);
|
|
162
|
+
if (name === "list") return args.length === 1 ? listOf(args[0]!) : listOf(DYN);
|
|
163
|
+
if (name === "map") return args.length === 2 ? mapOf(args[0]!, args[1]!) : mapOf(DYN, DYN);
|
|
164
|
+
if (name === "optional") {
|
|
165
|
+
if (args.length !== 1) {
|
|
166
|
+
throw new CelTypeExpressionError("optional takes exactly one type argument");
|
|
167
|
+
}
|
|
168
|
+
return optionalOf(args[0]!);
|
|
169
|
+
}
|
|
170
|
+
const primitive = PRIMITIVES[name];
|
|
171
|
+
if (primitive) return this.noArguments(name, args, primitive);
|
|
172
|
+
if (PARAMETER.test(name)) return this.noArguments(name, args, parameterOf(name));
|
|
173
|
+
const nominal = this.resolveNominal?.(name, args);
|
|
174
|
+
if (nominal) return nominal;
|
|
175
|
+
throw new CelUnknownTypeNameError(name);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private noArguments(name: string, args: readonly CelType[], type: CelType): CelType {
|
|
179
|
+
if (args.length > 0) throw new CelTypeExpressionError(`${name} takes no type arguments`);
|
|
180
|
+
return type;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Equality and ordering over values — **one comparison, serving both.**
|
|
3
|
+
*
|
|
4
|
+
* `<`, `<=`, `>`, `>=`, cross-numeric equality, `in` over a list and a list index all read
|
|
5
|
+
* the same rule, because two readers of "is this the same value" is how a container and an
|
|
6
|
+
* operator come to disagree about one expression.
|
|
7
|
+
*
|
|
8
|
+
* **Across the numeric types it CONVERTS.** The checker is strict — `1.0 == 1` is a type
|
|
9
|
+
* error — but `dyn(1.0) == 1` checks, so the runtime is reached with two numeric types in
|
|
10
|
+
* hand and cel-spec's answer is `true`. Where the double lies outside the integer type's
|
|
11
|
+
* range the sign decides; otherwise the integer becomes a double and two doubles are
|
|
12
|
+
* compared, **lossily**, so `dyn(9223372036854775807) < 9223372036854775808.0` is `false`.
|
|
13
|
+
* That is cel-spec's own rule: its corpus comments the case ("the conversion of the int to
|
|
14
|
+
* double is lossy") and names the test `not_lt_dyn_int_big_lossy_double`. `int(double)`
|
|
15
|
+
* refuses a double at or beyond **either** int64 extreme for the same reason.
|
|
16
|
+
*
|
|
17
|
+
* An exact comparison is defensible alone and indefensible as a cross-engine contract: a
|
|
18
|
+
* second engine built on a conformant library would answer the other way on a comparison
|
|
19
|
+
* that can decide an authorization or a retry bound, silently, at a level no static check
|
|
20
|
+
* reaches.
|
|
21
|
+
*
|
|
22
|
+
* **A map's key identity is NOT this comparison** and does not convert: a map is keyed by
|
|
23
|
+
* the canonical text of its key, so `{1u: 1.0}[?1.0]` reads the entry and `[?3.1]` names
|
|
24
|
+
* none.
|
|
25
|
+
*
|
|
26
|
+
* Equality across unrelated types is `false`, not an error — two values of different
|
|
27
|
+
* types are not equal, and asking is not a mistake. Ordering across unrelated types has
|
|
28
|
+
* no answer, so the caller reports no overload.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { durationNanos } from "./duration-value.js";
|
|
32
|
+
import { mapKeyIdentity } from "./cel-map-value.js";
|
|
33
|
+
import type { CelMapKey, CelValue } from "./cel-value.js";
|
|
34
|
+
import {
|
|
35
|
+
celTypeNameOf,
|
|
36
|
+
isCelBytes,
|
|
37
|
+
isCelDuration,
|
|
38
|
+
isCelMap,
|
|
39
|
+
isCelOptional,
|
|
40
|
+
isCelRecord,
|
|
41
|
+
isCelTimestamp,
|
|
42
|
+
isCelTypeValue,
|
|
43
|
+
isCelUint,
|
|
44
|
+
} from "./cel-value.js";
|
|
45
|
+
import { timestampNanos } from "./timestamp-value.js";
|
|
46
|
+
|
|
47
|
+
/** The int64 and uint64 extremes as doubles. Each reads as the nearest double there is. */
|
|
48
|
+
const MIN_INT_AS_DOUBLE = -9223372036854775808;
|
|
49
|
+
const MAX_INT_AS_DOUBLE = 9223372036854775807;
|
|
50
|
+
const MAX_UINT_AS_DOUBLE = 18446744073709551615;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Comparing an integer against a double, cel-spec's way: outside the integer type's range
|
|
54
|
+
* the sign decides, inside it both sides are doubles. Nothing for NaN, which orders with
|
|
55
|
+
* nothing including itself.
|
|
56
|
+
*/
|
|
57
|
+
function compareIntegerDouble(left: bigint, unsigned: boolean, right: number): number | undefined {
|
|
58
|
+
if (Number.isNaN(right)) return undefined;
|
|
59
|
+
if (right < (unsigned ? 0 : MIN_INT_AS_DOUBLE)) return 1;
|
|
60
|
+
if (right > (unsigned ? MAX_UINT_AS_DOUBLE : MAX_INT_AS_DOUBLE)) return -1;
|
|
61
|
+
const converted = Number(left);
|
|
62
|
+
return converted < right ? -1 : converted > right ? 1 : 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compareNumbers(left: number, right: number): number | undefined {
|
|
66
|
+
if (Number.isNaN(left) || Number.isNaN(right)) return undefined;
|
|
67
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compareBigints(left: bigint, right: bigint): number {
|
|
71
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The numeric magnitude of a value, where it has one, and which range it belongs to. */
|
|
75
|
+
function numeric(
|
|
76
|
+
value: CelValue,
|
|
77
|
+
): { integer: bigint; unsigned: boolean } | { double: number } | undefined {
|
|
78
|
+
if (typeof value === "bigint") return { integer: value, unsigned: false };
|
|
79
|
+
if (typeof value === "number") return { double: value };
|
|
80
|
+
if (isCelUint(value)) return { integer: value.value, unsigned: true };
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Whether two numeric values compare, and how. Nothing where one is NaN. */
|
|
85
|
+
function compareNumeric(left: CelValue, right: CelValue): number | undefined {
|
|
86
|
+
const a = numeric(left);
|
|
87
|
+
const b = numeric(right);
|
|
88
|
+
if (!a || !b) return undefined;
|
|
89
|
+
if ("integer" in a && "integer" in b) {
|
|
90
|
+
// An int and a uint are both exact integers, so one comparison serves both.
|
|
91
|
+
return compareBigints(a.integer, b.integer);
|
|
92
|
+
}
|
|
93
|
+
if ("integer" in a) {
|
|
94
|
+
return compareIntegerDouble(a.integer, a.unsigned, (b as { double: number }).double);
|
|
95
|
+
}
|
|
96
|
+
if ("integer" in b) {
|
|
97
|
+
const compared = compareIntegerDouble(b.integer, b.unsigned, a.double);
|
|
98
|
+
return compared === undefined ? undefined : -compared;
|
|
99
|
+
}
|
|
100
|
+
return compareNumbers(a.double, (b as { double: number }).double);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareBytes(left: Uint8Array, right: Uint8Array): number {
|
|
104
|
+
const shared = Math.min(left.length, right.length);
|
|
105
|
+
for (let at = 0; at < shared; at += 1) {
|
|
106
|
+
if (left[at]! !== right[at]!) return left[at]! < right[at]! ? -1 : 1;
|
|
107
|
+
}
|
|
108
|
+
return left.length === right.length ? 0 : left.length < right.length ? -1 : 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How two values order: `-1`, `0`, `1`, or nothing when they do not order at all —
|
|
113
|
+
* unrelated types, or a NaN, which orders with nothing including itself.
|
|
114
|
+
*/
|
|
115
|
+
export function celCompare(left: CelValue, right: CelValue): number | undefined {
|
|
116
|
+
if (numeric(left) && numeric(right)) return compareNumeric(left, right);
|
|
117
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
118
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
if (typeof left === "boolean" && typeof right === "boolean") {
|
|
121
|
+
return left === right ? 0 : left ? 1 : -1;
|
|
122
|
+
}
|
|
123
|
+
if (isCelBytes(left) && isCelBytes(right)) return compareBytes(left, right);
|
|
124
|
+
if (isCelTimestamp(left) && isCelTimestamp(right)) {
|
|
125
|
+
return compareBigints(timestampNanos(left), timestampNanos(right));
|
|
126
|
+
}
|
|
127
|
+
if (isCelDuration(left) && isCelDuration(right)) {
|
|
128
|
+
return compareBigints(durationNanos(left), durationNanos(right));
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** CEL equality: by value, across the numeric types, and `false` across unrelated ones. */
|
|
134
|
+
export function celEqual(left: CelValue, right: CelValue): boolean {
|
|
135
|
+
// One identical primitive is equal to itself, and the two edges agree: `-0 === 0` and CEL
|
|
136
|
+
// says they are equal, `NaN !== NaN` and CEL says they are not — so the slow path below
|
|
137
|
+
// answers NaN correctly after this check declines it.
|
|
138
|
+
if (left === right && typeof left !== "object") return true;
|
|
139
|
+
if (numeric(left) && numeric(right)) return compareNumeric(left, right) === 0;
|
|
140
|
+
if (left === null || right === null) return left === right;
|
|
141
|
+
const leftType = celTypeNameOf(left);
|
|
142
|
+
if (leftType !== celTypeNameOf(right)) return false;
|
|
143
|
+
switch (leftType) {
|
|
144
|
+
case "bool":
|
|
145
|
+
case "string":
|
|
146
|
+
return left === right;
|
|
147
|
+
case "bytes":
|
|
148
|
+
return compareBytes(left as Uint8Array, right as Uint8Array) === 0;
|
|
149
|
+
case "google.protobuf.Timestamp":
|
|
150
|
+
return timestampNanos(left as never) === timestampNanos(right as never);
|
|
151
|
+
case "google.protobuf.Duration":
|
|
152
|
+
return durationNanos(left as never) === durationNanos(right as never);
|
|
153
|
+
case "type":
|
|
154
|
+
return isCelTypeValue(left) && isCelTypeValue(right) && left.name === right.name;
|
|
155
|
+
case "list":
|
|
156
|
+
return equalLists(left as readonly CelValue[], right as readonly CelValue[]);
|
|
157
|
+
case "map":
|
|
158
|
+
return equalMaps(left, right);
|
|
159
|
+
case "optional":
|
|
160
|
+
return equalOptionals(left, right);
|
|
161
|
+
default:
|
|
162
|
+
// A host's own named type: equal when the host's operator says so, which it
|
|
163
|
+
// registers itself. With none registered, identity is all this engine knows.
|
|
164
|
+
return left === right;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function equalLists(left: readonly CelValue[], right: readonly CelValue[]): boolean {
|
|
169
|
+
if (left.length !== right.length) return false;
|
|
170
|
+
return left.every((value, at) => celEqual(value, right[at]!));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Every key of a map, as the pairs equality walks. */
|
|
174
|
+
function mapPairs(value: CelValue): Map<CelMapKey, CelValue> | undefined {
|
|
175
|
+
if (isCelMap(value)) {
|
|
176
|
+
return new Map([...value.entries].map(([identity, entry]) => [identity, entry.value]));
|
|
177
|
+
}
|
|
178
|
+
if (isCelRecord(value)) {
|
|
179
|
+
const pairs = new Map<CelMapKey, CelValue>();
|
|
180
|
+
for (const key of Object.keys(value)) pairs.set(mapKeyIdentity(key)!, value[key] as CelValue);
|
|
181
|
+
return pairs;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function equalMaps(left: CelValue, right: CelValue): boolean {
|
|
187
|
+
const a = mapPairs(left);
|
|
188
|
+
const b = mapPairs(right);
|
|
189
|
+
if (!a || !b || a.size !== b.size) return false;
|
|
190
|
+
for (const [identity, value] of a) {
|
|
191
|
+
if (!b.has(identity)) return false;
|
|
192
|
+
if (!celEqual(value, b.get(identity)!)) return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function equalOptionals(left: CelValue, right: CelValue): boolean {
|
|
198
|
+
if (!isCelOptional(left) || !isCelOptional(right)) return false;
|
|
199
|
+
if (left.present !== right.present) return false;
|
|
200
|
+
return !left.present || celEqual(left.held as CelValue, right.held as CelValue);
|
|
201
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text: how a value is written as a string, and how text becomes bytes.
|
|
3
|
+
*
|
|
4
|
+
* `string()` is the one conversion a consumer renders a value through — an interpolated
|
|
5
|
+
* hole is defined as the CEL join of `string(<hole>)` — so each form here is the value's
|
|
6
|
+
* single written form, the same one the plain encoding uses: RFC 3339 for an instant,
|
|
7
|
+
* seconds with an `s` for a duration, UTF-8 text for bytes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { celError, type CelError } from "./cel-value.js";
|
|
11
|
+
import type { SourceRange } from "./syntax-tree.js";
|
|
12
|
+
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
15
|
+
|
|
16
|
+
/** The UTF-8 bytes of text. */
|
|
17
|
+
export function textToBytes(text: string): Uint8Array {
|
|
18
|
+
return encoder.encode(text);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The text bytes hold. Bytes that are not UTF-8 are a conversion error rather than text
|
|
23
|
+
* with a replacement character in it: a replacement character is a value the author
|
|
24
|
+
* never wrote, and it would read back as different bytes.
|
|
25
|
+
*/
|
|
26
|
+
export function bytesToText(bytes: Uint8Array, range?: SourceRange): string | CelError {
|
|
27
|
+
try {
|
|
28
|
+
return decoder.decode(bytes);
|
|
29
|
+
} catch {
|
|
30
|
+
return celError("invalid_conversion", "the bytes are not UTF-8 text", range);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A double as text. The shortest digits that read back as the same double, which is the
|
|
36
|
+
* host's own shortest-round-trip form; the non-finite doubles are named as cel-spec
|
|
37
|
+
* writes them.
|
|
38
|
+
*/
|
|
39
|
+
export function doubleText(value: number): string {
|
|
40
|
+
if (Number.isNaN(value)) return "NaN";
|
|
41
|
+
if (value === Number.POSITIVE_INFINITY) return "+Inf";
|
|
42
|
+
if (value === Number.NEGATIVE_INFINITY) return "-Inf";
|
|
43
|
+
if (value === 0 && Object.is(value, -0)) return "-0";
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calendar in a time zone: an instant's wall-clock fields, and the instant a wall
|
|
3
|
+
* clock names.
|
|
4
|
+
*
|
|
5
|
+
* An instant is only a date once a zone is chosen, so every calendar function of the
|
|
6
|
+
* function catalog reads its fields through here. The zone's RULES are the host's data
|
|
7
|
+
* (`Intl.DateTimeFormat`, ECMA-402, present in a browser as in Node), never this
|
|
8
|
+
* engine's: what is written here is only how those rules are applied, which is the half
|
|
9
|
+
* a second engine has to reproduce.
|
|
10
|
+
*
|
|
11
|
+
* It is separate from `timestamp-value.ts` on purpose. That file is the language's own
|
|
12
|
+
* zone reading — cel-spec's getters, which also accept a fixed `HH:MM` offset and word
|
|
13
|
+
* their refusal as the language does. This one is the catalog's: it accepts an IANA name
|
|
14
|
+
* alone, it is millisecond-granular because the functions built on it render
|
|
15
|
+
* milliseconds, and it answers a gap or a repeated wall clock the way a calendar
|
|
16
|
+
* library does. Folding the two would make one of the two behaviours wrong.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A wall clock, to the second: what a zone makes of an instant. */
|
|
20
|
+
export interface CivilTime {
|
|
21
|
+
year: number;
|
|
22
|
+
month: number;
|
|
23
|
+
day: number;
|
|
24
|
+
hour: number;
|
|
25
|
+
minute: number;
|
|
26
|
+
second: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the host knows the zone. The catalog's refusal is worded by its caller, so
|
|
31
|
+
* this answers only yes or no — left to `Intl` the failure is a `RangeError` naming
|
|
32
|
+
* neither the function nor what was wrong with the argument.
|
|
33
|
+
*/
|
|
34
|
+
export function knownTimeZone(zone: string): boolean {
|
|
35
|
+
try {
|
|
36
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const FIELD_OPTIONS: Intl.DateTimeFormatOptions = {
|
|
44
|
+
hourCycle: "h23",
|
|
45
|
+
year: "numeric",
|
|
46
|
+
month: "2-digit",
|
|
47
|
+
day: "2-digit",
|
|
48
|
+
hour: "2-digit",
|
|
49
|
+
minute: "2-digit",
|
|
50
|
+
second: "2-digit",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function zoneParts(
|
|
54
|
+
instant: number,
|
|
55
|
+
zone: string,
|
|
56
|
+
options: Intl.DateTimeFormatOptions,
|
|
57
|
+
): Record<string, string> {
|
|
58
|
+
const parts: Record<string, string> = {};
|
|
59
|
+
for (const part of new Intl.DateTimeFormat("en-US", { timeZone: zone, ...options }).formatToParts(
|
|
60
|
+
new Date(instant),
|
|
61
|
+
)) {
|
|
62
|
+
parts[part.type] = part.value;
|
|
63
|
+
}
|
|
64
|
+
return parts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The wall clock an instant reads as in a zone. */
|
|
68
|
+
export function civilTimeIn(instant: number, zone: string): CivilTime {
|
|
69
|
+
const parts = zoneParts(instant, zone, FIELD_OPTIONS);
|
|
70
|
+
return {
|
|
71
|
+
year: +parts.year!,
|
|
72
|
+
month: +parts.month!,
|
|
73
|
+
day: +parts.day!,
|
|
74
|
+
hour: +parts.hour!,
|
|
75
|
+
minute: +parts.minute!,
|
|
76
|
+
second: +parts.second!,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The zone's offset at an instant, in milliseconds: its wall clock read as UTC, minus it. */
|
|
81
|
+
function offsetAt(instant: number, zone: string): number {
|
|
82
|
+
const fields = civilTimeIn(instant, zone);
|
|
83
|
+
return (
|
|
84
|
+
Date.UTC(fields.year, fields.month - 1, fields.day, fields.hour, fields.minute, fields.second) -
|
|
85
|
+
instant
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sameWallClock(left: CivilTime, right: CivilTime): boolean {
|
|
90
|
+
return (
|
|
91
|
+
left.year === right.year &&
|
|
92
|
+
left.month === right.month &&
|
|
93
|
+
left.day === right.day &&
|
|
94
|
+
left.hour === right.hour &&
|
|
95
|
+
left.minute === right.minute &&
|
|
96
|
+
left.second === right.second
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The instant whose wall clock in `zone` is these fields.
|
|
102
|
+
*
|
|
103
|
+
* The offset depends on the instant being solved for, so this takes the offset at the
|
|
104
|
+
* UTC reading, corrects, and then **checks** by reading the result back. That check is
|
|
105
|
+
* the whole point: a plain fixpoint settles on an instant whose wall clock is not the one
|
|
106
|
+
* asked for whenever the requested time does not exist, and it settles BACKWARDS — which
|
|
107
|
+
* silently moves the calendar day, the one thing `addMonths` and `startOfMonth` exist to
|
|
108
|
+
* control. Chile jumps 00:00 to 01:00 on 2026-09-06 and Cuba on 2026-03-08, so "the 6th
|
|
109
|
+
* at midnight" there is not a time; a fixpoint answered "the 5th at 23:00".
|
|
110
|
+
*
|
|
111
|
+
* Resolution follows Java's `ZonedDateTime` and Temporal's `compatible`: a wall clock
|
|
112
|
+
* that exists twice (a fall-back) takes the EARLIER instant, and one that does not exist
|
|
113
|
+
* (a spring-forward gap) shifts FORWARD out of the gap, which keeps the requested day.
|
|
114
|
+
*/
|
|
115
|
+
export function instantOfCivilTime(fields: CivilTime, zone: string): number {
|
|
116
|
+
const asUtc = Date.UTC(
|
|
117
|
+
fields.year,
|
|
118
|
+
fields.month - 1,
|
|
119
|
+
fields.day,
|
|
120
|
+
fields.hour,
|
|
121
|
+
fields.minute,
|
|
122
|
+
fields.second,
|
|
123
|
+
);
|
|
124
|
+
const offsetA = offsetAt(asUtc, zone);
|
|
125
|
+
const candidateA = asUtc - offsetA;
|
|
126
|
+
const offsetB = offsetAt(candidateA, zone);
|
|
127
|
+
if (offsetA === offsetB) return candidateA;
|
|
128
|
+
|
|
129
|
+
const candidateB = asUtc - offsetB;
|
|
130
|
+
const aHolds = sameWallClock(civilTimeIn(candidateA, zone), fields);
|
|
131
|
+
const bHolds = sameWallClock(civilTimeIn(candidateB, zone), fields);
|
|
132
|
+
if (aHolds && bHolds) return Math.min(candidateA, candidateB);
|
|
133
|
+
if (aHolds) return candidateA;
|
|
134
|
+
if (bHolds) return candidateB;
|
|
135
|
+
return Math.max(candidateA, candidateB);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `Date.UTC` maps years 0-99 to 1900-1999, so the year is set explicitly. */
|
|
139
|
+
export function daysInMonth(year: number, month: number): number {
|
|
140
|
+
const date = new Date(Date.UTC(2000, month, 0));
|
|
141
|
+
date.setUTCFullYear(year, month, 0);
|
|
142
|
+
return date.getUTCDate();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function pad(value: number, width: number): string {
|
|
146
|
+
return String(value).padStart(width, "0");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The calendar date (`YYYY-MM-DD`) an instant reads as in a zone. */
|
|
150
|
+
export function dateTextIn(instant: number, zone: string): string {
|
|
151
|
+
if (zone === "UTC" || zone === "Z") return new Date(instant).toISOString().slice(0, 10);
|
|
152
|
+
const parts = zoneParts(instant, zone, { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
153
|
+
return `${parts.year}-${parts.month}-${parts.day}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* An instant as ISO-8601 in a zone, with milliseconds: UTC as `…Z`, else the zone's own
|
|
158
|
+
* offset (`2026-06-06T18:30:00.000-05:00`). The offset is derived arithmetically from
|
|
159
|
+
* standard `Intl` fields, so nothing here needs a newer ECMA-402 surface.
|
|
160
|
+
*/
|
|
161
|
+
export function isoTextIn(instant: number, zone: string): string {
|
|
162
|
+
if (zone === "UTC" || zone === "Z") return new Date(instant).toISOString();
|
|
163
|
+
const fields = civilTimeIn(instant, zone);
|
|
164
|
+
// Sub-second is zone-independent; read it off the instant directly.
|
|
165
|
+
const milliseconds = pad(((instant % 1000) + 1000) % 1000, 3);
|
|
166
|
+
const minutes = Math.round(
|
|
167
|
+
(Date.UTC(fields.year, fields.month - 1, fields.day, fields.hour, fields.minute, fields.second) -
|
|
168
|
+
instant) /
|
|
169
|
+
60000,
|
|
170
|
+
);
|
|
171
|
+
const offset =
|
|
172
|
+
minutes === 0
|
|
173
|
+
? "Z"
|
|
174
|
+
: `${minutes > 0 ? "+" : "-"}${pad(Math.floor(Math.abs(minutes) / 60), 2)}:${pad(Math.abs(minutes) % 60, 2)}`;
|
|
175
|
+
const date = `${pad(fields.year, 4)}-${pad(fields.month, 2)}-${pad(fields.day, 2)}`;
|
|
176
|
+
const time = `${pad(fields.hour, 2)}:${pad(fields.minute, 2)}:${pad(fields.second, 2)}`;
|
|
177
|
+
return `${date}T${time}.${milliseconds}${offset}`;
|
|
178
|
+
}
|