@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a comprehension macro **does**, as a function of its range and its body.
|
|
3
|
+
*
|
|
4
|
+
* The macros are lowered by each backend — there is no comprehension node in the tree,
|
|
5
|
+
* because expanding one at read time would make the source unwritable from the tree —
|
|
6
|
+
* but what the lowering *means* lives here, once. The closure backend passes a closure
|
|
7
|
+
* as the body and the emitter will pass an emitted function, and both get the same
|
|
8
|
+
* answers, including the error rules.
|
|
9
|
+
*
|
|
10
|
+
* **An error does not win over a decided answer**, with the one exception named below: `all`
|
|
11
|
+
* is `false` as soon as one element is false even if another errors, and `exists` is `true` as
|
|
12
|
+
* soon as one is true. That is the same rule `&&` and `||` follow, and it is what makes a
|
|
13
|
+
* comprehension over partly-unreadable data usable at all.
|
|
14
|
+
*
|
|
15
|
+
* **An element entering a body is a door a host value comes through**, and it is checked for a
|
|
16
|
+
* thenable here — in the one place a comprehension's meaning lives, so both backends inherit it
|
|
17
|
+
* rather than each remembering to. It is not the leak it looks like: `xs.all(e, true)` over a
|
|
18
|
+
* host list holding a promise used to answer **`true`**, having decided something about a value
|
|
19
|
+
* it never touched, which is the worse half of the failure class. The cost is one `typeof` per
|
|
20
|
+
* element, the same check the member read already makes, on a loop that already calls a closure
|
|
21
|
+
* per element.
|
|
22
|
+
*
|
|
23
|
+
* **That refusal is TERMINAL, and it is the one error here that does not short-circuit.** It is
|
|
24
|
+
* checked where the element is BOUND, before the body runs, and it ends the comprehension
|
|
25
|
+
* whatever a later element would have decided — so `xs.all(e, false)` and `xs.exists(e, true)`
|
|
26
|
+
* refuse rather than answering `false` and `true` off the one element that is readable. The
|
|
27
|
+
* distinction is what the error is ABOUT: `no_such_key` is a fact about one datum, so a decided
|
|
28
|
+
* answer may legitimately outrank it, while a value that must be awaited says the host handed
|
|
29
|
+
* this engine something it cannot evaluate at all. Carrying that past a convenient element makes
|
|
30
|
+
* the answer depend on how many elements happen to be readable and in what order — and the rule
|
|
31
|
+
* is subtle enough that two readers in a row took the discardable refusal for a leak instead.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { CelValue } from "./cel-value.js";
|
|
35
|
+
import { asyncValueRefused, celError, isCelError, type CelError } from "./cel-value.js";
|
|
36
|
+
import type { SourceRange } from "./syntax-tree.js";
|
|
37
|
+
|
|
38
|
+
/** The body of a comprehension: the value its expression answers for one element. */
|
|
39
|
+
export type ComprehensionBody = (element: CelValue) => CelValue;
|
|
40
|
+
|
|
41
|
+
function notBool(value: CelValue, range?: SourceRange): CelError {
|
|
42
|
+
return celError(
|
|
43
|
+
"no_matching_overload",
|
|
44
|
+
`a comprehension's test must answer a bool, and it answered ${typeof value}`,
|
|
45
|
+
range,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether an element entering a body is one that must be awaited.
|
|
51
|
+
*
|
|
52
|
+
* The `typeof` is inlined here rather than left to the call, because this runs once per element
|
|
53
|
+
* on the engine's hottest loop and only an OBJECT can be a thenable. Measured on a
|
|
54
|
+
* 100k-element `map` over integers — a body that does almost nothing, so the check is as
|
|
55
|
+
* visible as it ever gets: calling unconditionally cost about 4 ms, and with the `typeof`
|
|
56
|
+
* guarding the call the difference sits at this machine's noise floor.
|
|
57
|
+
*/
|
|
58
|
+
function awaited(element: CelValue, range?: SourceRange): CelError | undefined {
|
|
59
|
+
if (typeof element !== "object" || element === null) return undefined;
|
|
60
|
+
return asyncValueRefused(element, range);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A predicate's answer for one element: `true`, `false`, or the error it is. */
|
|
64
|
+
function test(body: ComprehensionBody, element: CelValue, range?: SourceRange): boolean | CelError {
|
|
65
|
+
const answered = body(element);
|
|
66
|
+
if (isCelError(answered)) return answered;
|
|
67
|
+
if (typeof answered !== "boolean") return notBool(answered, range);
|
|
68
|
+
return answered;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function celAll(
|
|
72
|
+
elements: readonly CelValue[],
|
|
73
|
+
body: ComprehensionBody,
|
|
74
|
+
range?: SourceRange,
|
|
75
|
+
): CelValue {
|
|
76
|
+
let failure: CelError | undefined;
|
|
77
|
+
for (const element of elements) {
|
|
78
|
+
const refused = awaited(element, range);
|
|
79
|
+
if (refused) return refused;
|
|
80
|
+
const answered = test(body, element, range);
|
|
81
|
+
if (answered === false) return false;
|
|
82
|
+
if (answered !== true && !failure) failure = answered;
|
|
83
|
+
}
|
|
84
|
+
return failure ?? true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function celExists(
|
|
88
|
+
elements: readonly CelValue[],
|
|
89
|
+
body: ComprehensionBody,
|
|
90
|
+
range?: SourceRange,
|
|
91
|
+
): CelValue {
|
|
92
|
+
let failure: CelError | undefined;
|
|
93
|
+
for (const element of elements) {
|
|
94
|
+
const refused = awaited(element, range);
|
|
95
|
+
if (refused) return refused;
|
|
96
|
+
const answered = test(body, element, range);
|
|
97
|
+
if (answered === true) return true;
|
|
98
|
+
if (answered !== false && !failure) failure = answered;
|
|
99
|
+
}
|
|
100
|
+
return failure ?? false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Exactly one element satisfies the test. An error anywhere leaves no count to trust. */
|
|
104
|
+
export function celExistsOne(
|
|
105
|
+
elements: readonly CelValue[],
|
|
106
|
+
body: ComprehensionBody,
|
|
107
|
+
range?: SourceRange,
|
|
108
|
+
): CelValue {
|
|
109
|
+
let found = 0;
|
|
110
|
+
for (const element of elements) {
|
|
111
|
+
const refused = awaited(element, range);
|
|
112
|
+
if (refused) return refused;
|
|
113
|
+
const answered = test(body, element, range);
|
|
114
|
+
if (isCelError(answered)) return answered;
|
|
115
|
+
if (answered) found += 1;
|
|
116
|
+
}
|
|
117
|
+
return found === 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function celFilter(
|
|
121
|
+
elements: readonly CelValue[],
|
|
122
|
+
body: ComprehensionBody,
|
|
123
|
+
range?: SourceRange,
|
|
124
|
+
): CelValue {
|
|
125
|
+
const kept: CelValue[] = [];
|
|
126
|
+
for (const element of elements) {
|
|
127
|
+
const refused = awaited(element, range);
|
|
128
|
+
if (refused) return refused;
|
|
129
|
+
const answered = test(body, element, range);
|
|
130
|
+
if (isCelError(answered)) return answered;
|
|
131
|
+
if (answered) kept.push(element);
|
|
132
|
+
}
|
|
133
|
+
return kept;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `map` in both arities: with a filter, an element the filter drops is not transformed. */
|
|
137
|
+
export function celMapComprehension(
|
|
138
|
+
elements: readonly CelValue[],
|
|
139
|
+
transform: ComprehensionBody,
|
|
140
|
+
filter?: ComprehensionBody,
|
|
141
|
+
range?: SourceRange,
|
|
142
|
+
): CelValue {
|
|
143
|
+
const out: CelValue[] = [];
|
|
144
|
+
for (const element of elements) {
|
|
145
|
+
const refused = awaited(element, range);
|
|
146
|
+
if (refused) return refused;
|
|
147
|
+
if (filter) {
|
|
148
|
+
const answered = test(filter, element, range);
|
|
149
|
+
if (isCelError(answered)) return answered;
|
|
150
|
+
if (!answered) continue;
|
|
151
|
+
}
|
|
152
|
+
const value = transform(element);
|
|
153
|
+
if (isCelError(value)) return value;
|
|
154
|
+
out.push(value);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splitting a dotted chain over the names a host DECLARED — once, for both halves.
|
|
3
|
+
*
|
|
4
|
+
* A host may declare `a.b.c`, or `a.b` holding a map, or both. `a.b.c` then reads the
|
|
5
|
+
* variable of that name where it is declared and the map's entry where only `a.b` is, so the
|
|
6
|
+
* host's own naming decides rather than the shape of the expression: the **longest declared
|
|
7
|
+
* prefix wins**.
|
|
8
|
+
*
|
|
9
|
+
* The checker and the backend ask the same question of the same declarations, so they ask it
|
|
10
|
+
* here. Two readers would be two answers to "which name is this chain", and the pair that
|
|
11
|
+
* disagrees is the worst kind: `telo check` types one name while evaluation reads another, and
|
|
12
|
+
* nothing says so. Splitting at compile time also means a declared chain costs **one**
|
|
13
|
+
* activation read at evaluation rather than a search over its prefixes.
|
|
14
|
+
*
|
|
15
|
+
* A chain **no prefix of which** is declared — the root included — is left alone: there is
|
|
16
|
+
* nothing to split it on, and the checker has no opinion about it either. The backend then
|
|
17
|
+
* searches the activation at evaluation, which is what every conformance row binding a dotted
|
|
18
|
+
* key relies on. Where any prefix IS declared the search never runs, so a key the host did not
|
|
19
|
+
* declare can never win over a name it did.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Where a chain splits: the declared name, and the members read off its value. */
|
|
23
|
+
export interface DeclaredChain {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly rest: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The longest declared prefix of a chain, or nothing when no declaration covers one.
|
|
30
|
+
*
|
|
31
|
+
* **The root counts as a prefix**, and leaving it out was the disagreement this function
|
|
32
|
+
* exists to prevent: with a chain whose root alone is declared treated as undeclared, the
|
|
33
|
+
* backend fell into the activation prefix search, and an activation holding both `a` and the
|
|
34
|
+
* key `"a.b"` answered the undeclared key where the checker had typed the declared name.
|
|
35
|
+
*/
|
|
36
|
+
export function splitDeclaredChain(
|
|
37
|
+
segments: readonly string[],
|
|
38
|
+
declared: (name: string) => boolean,
|
|
39
|
+
): DeclaredChain | undefined {
|
|
40
|
+
for (let length = segments.length; length >= 1; length -= 1) {
|
|
41
|
+
const name = segments.slice(0, length).join(".");
|
|
42
|
+
if (declared(name)) return { name, rest: segments.slice(length) };
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durations: reading one, writing one, and the fields of one.
|
|
3
|
+
*
|
|
4
|
+
* **CEL's duration is the signed 64-bit range of its total NANOSECONDS** —
|
|
5
|
+
* `-9223372036.854775808s … 9223372036.854775807s`, roughly ±292 years. That is cel-spec's
|
|
6
|
+
* own rule, stated under *Overflow* in its language definition ("Duration values are
|
|
7
|
+
* limited to a single int64 value, or roughly +-290 years"), and all six of the vectors'
|
|
8
|
+
* `timestamps/duration_range` rows read it: each uses an operand of 200,000,000,000s or
|
|
9
|
+
* 320,000,000,000s and each expects `duration out of range`.
|
|
10
|
+
*
|
|
11
|
+
* **It is a subrange of the typed frame's, deliberately.** The frame carries protobuf's
|
|
12
|
+
* `google.protobuf.Duration` range — ±315,576,000,000s, ±10,000 years — because a duration
|
|
13
|
+
* arriving from a transport, a journal or a controller must always be representable, and
|
|
14
|
+
* narrowing the frame to CEL's range would make such a value unwritable. So CEL ⊂ frame:
|
|
15
|
+
* `duration('200000000000s')` is a value the frame carries and CEL cannot construct, which
|
|
16
|
+
* is the correct answer rather than a cost. Nothing about the frame changes here.
|
|
17
|
+
*
|
|
18
|
+
* The range is an invariant, not a storage format: a duration is still seconds and
|
|
19
|
+
* nanoseconds of the same sign, with `|nanos| < 1e9`, and nanosecond-precise. A computation
|
|
20
|
+
* that leaves the range is an error, never a value outside it.
|
|
21
|
+
*
|
|
22
|
+
* It is written as seconds with an `s` suffix (`1000000s`, `-1.5s`), which is cel-spec's
|
|
23
|
+
* own spelling, and the one plain encoding a duration has wherever it is written down.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { celError, CEL_VALUE_TYPE, isCelError, type CelDuration, type CelError } from "./cel-value.js";
|
|
27
|
+
import type { SourceRange } from "./syntax-tree.js";
|
|
28
|
+
|
|
29
|
+
/** The widest and narrowest total a duration holds, in nanoseconds: int64. */
|
|
30
|
+
export const MAX_DURATION_NANOS = 9_223_372_036_854_775_807n;
|
|
31
|
+
export const MIN_DURATION_NANOS = -9_223_372_036_854_775_808n;
|
|
32
|
+
|
|
33
|
+
const NANOS_PER_SECOND = 1_000_000_000n;
|
|
34
|
+
|
|
35
|
+
/** A duration from a total in nanoseconds, or the range error. */
|
|
36
|
+
export function celDurationFromNanos(total: bigint, range?: SourceRange): CelDuration | CelError {
|
|
37
|
+
if (total > MAX_DURATION_NANOS || total < MIN_DURATION_NANOS) {
|
|
38
|
+
return celError("invalid_conversion", "duration out of range", range);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
[CEL_VALUE_TYPE]: "google.protobuf.Duration",
|
|
42
|
+
seconds: total / NANOS_PER_SECOND,
|
|
43
|
+
nanos: Number(total % NANOS_PER_SECOND),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The error a duration **this engine did not build** is outside CEL's range: one from a
|
|
49
|
+
* host, which the frame's wider range admits. Checked where such a duration is used, not
|
|
50
|
+
* where it arrives, so a value merely passing through is not judged.
|
|
51
|
+
*/
|
|
52
|
+
export function durationOutOfRange(value: CelDuration, range?: SourceRange): CelError | undefined {
|
|
53
|
+
const total = durationNanos(value);
|
|
54
|
+
if (total <= MAX_DURATION_NANOS && total >= MIN_DURATION_NANOS) return undefined;
|
|
55
|
+
return celError("invalid_conversion", "duration out of range", range);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The duration's total nanoseconds — what arithmetic and ordering compare. */
|
|
59
|
+
export function durationNanos(value: CelDuration): bigint {
|
|
60
|
+
return value.seconds * NANOS_PER_SECOND + BigInt(value.nanos);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const UNIT_NANOS: Readonly<Record<string, bigint>> = {
|
|
64
|
+
ns: 1n,
|
|
65
|
+
us: 1_000n,
|
|
66
|
+
"µs": 1_000n,
|
|
67
|
+
"μs": 1_000n,
|
|
68
|
+
ms: 1_000_000n,
|
|
69
|
+
s: 1_000_000_000n,
|
|
70
|
+
m: 60_000_000_000n,
|
|
71
|
+
h: 3_600_000_000_000n,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const PART = /([0-9]*)(?:\.([0-9]*))?(ns|us|µs|μs|ms|s|m|h)/g;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A duration from CEL's own text: a sign, then one or more number-and-unit parts over
|
|
78
|
+
* `ns`, `us`, `ms`, `s`, `m` and `h` (`1h30m`, `1.5s`, `-10m`).
|
|
79
|
+
*/
|
|
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 {
|
|
96
|
+
const refuse = () =>
|
|
97
|
+
celError("invalid_conversion", `${JSON.stringify(text)} is not a duration`, range);
|
|
98
|
+
let body = text;
|
|
99
|
+
let negative = false;
|
|
100
|
+
if (body.startsWith("+") || body.startsWith("-")) {
|
|
101
|
+
negative = body.startsWith("-");
|
|
102
|
+
body = body.slice(1);
|
|
103
|
+
}
|
|
104
|
+
if (body === "0") return 0n;
|
|
105
|
+
if (body === "") return refuse();
|
|
106
|
+
PART.lastIndex = 0;
|
|
107
|
+
let total = 0n;
|
|
108
|
+
let at = 0;
|
|
109
|
+
for (let found = PART.exec(body); found; found = PART.exec(body)) {
|
|
110
|
+
if (found.index !== at) return refuse();
|
|
111
|
+
const [whole, digits, fraction, unit] = found;
|
|
112
|
+
if (!digits && !fraction) return refuse();
|
|
113
|
+
const scale = UNIT_NANOS[unit!]!;
|
|
114
|
+
total += BigInt(digits || "0") * scale;
|
|
115
|
+
if (fraction) {
|
|
116
|
+
const padded = `${fraction}000000000`.slice(0, 9);
|
|
117
|
+
total += (BigInt(padded) * scale) / NANOS_PER_SECOND;
|
|
118
|
+
}
|
|
119
|
+
at += whole.length;
|
|
120
|
+
}
|
|
121
|
+
if (at !== body.length) return refuse();
|
|
122
|
+
return negative ? -total : total;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Seconds with an `s` suffix, the fraction trimmed to what it carries. */
|
|
126
|
+
export function formatDuration(value: CelDuration): string {
|
|
127
|
+
const total = durationNanos(value);
|
|
128
|
+
const negative = total < 0n;
|
|
129
|
+
const magnitude = negative ? -total : total;
|
|
130
|
+
const seconds = magnitude / NANOS_PER_SECOND;
|
|
131
|
+
const nanos = magnitude % NANOS_PER_SECOND;
|
|
132
|
+
const fraction = nanos === 0n ? "" : `.${String(nanos).padStart(9, "0").replace(/0+$/, "")}`;
|
|
133
|
+
return `${negative ? "-" : ""}${seconds}${fraction}s`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Which field of a duration a getter answers. */
|
|
137
|
+
export type DurationField = "getHours" | "getMinutes" | "getSeconds" | "getMilliseconds";
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A field of a duration. `getHours`, `getMinutes` and `getSeconds` answer the **whole span**
|
|
141
|
+
* in that unit, truncated toward zero; `getMilliseconds` answers the **component** — the
|
|
142
|
+
* milliseconds inside the second, so `duration('123.321456789s').getMilliseconds()` is 321
|
|
143
|
+
* and not 123,321.
|
|
144
|
+
*
|
|
145
|
+
* That split is cel-spec's, and it is also the only reading under which the engine does not
|
|
146
|
+
* contradict itself: a timestamp's `getMilliseconds()` has always answered the component.
|
|
147
|
+
*/
|
|
148
|
+
export function durationField(value: CelDuration, field: DurationField): bigint {
|
|
149
|
+
const total = durationNanos(value);
|
|
150
|
+
switch (field) {
|
|
151
|
+
case "getHours":
|
|
152
|
+
return total / 3_600_000_000_000n;
|
|
153
|
+
case "getMinutes":
|
|
154
|
+
return total / 60_000_000_000n;
|
|
155
|
+
case "getSeconds":
|
|
156
|
+
return total / NANOS_PER_SECOND;
|
|
157
|
+
case "getMilliseconds":
|
|
158
|
+
return BigInt(Math.trunc(value.nanos / 1_000_000));
|
|
159
|
+
}
|
|
160
|
+
}
|