@telorun/cel 0.107.0 → 0.109.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/catalog-runtime.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { LiteralArgumentCheck } from "./signature.js";
|
|
|
34
34
|
import type {
|
|
35
35
|
CelEvaluationCode,
|
|
36
36
|
CelMap,
|
|
37
|
+
CelMapKey,
|
|
37
38
|
CelMapValueEntry,
|
|
38
39
|
CelRecord,
|
|
39
40
|
CelTimestamp,
|
|
@@ -104,10 +105,10 @@ class CatalogRefusal extends Error {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/** The one place a refusal becomes a value. Anything else thrown is a defect here. */
|
|
107
|
-
function refusing(run:
|
|
108
|
-
return (
|
|
108
|
+
function refusing(run: CelImplementation): CelImplementation {
|
|
109
|
+
return (ctx, a, b, c, d) => {
|
|
109
110
|
try {
|
|
110
|
-
return run(
|
|
111
|
+
return run(ctx, a, b, c, d);
|
|
111
112
|
} catch (cause) {
|
|
112
113
|
if (cause instanceof CatalogRefusal) return celError(cause.code, cause.message, ctx.range);
|
|
113
114
|
throw cause;
|
|
@@ -117,7 +118,7 @@ function refusing(run: (args: readonly CelValue[], ctx: CelCallContext) => CelVa
|
|
|
117
118
|
|
|
118
119
|
/** An implementation that needs nothing of the host. */
|
|
119
120
|
const own =
|
|
120
|
-
(run:
|
|
121
|
+
(run: CelImplementation): CatalogImplementation =>
|
|
121
122
|
() =>
|
|
122
123
|
refusing(run);
|
|
123
124
|
|
|
@@ -129,14 +130,12 @@ const own =
|
|
|
129
130
|
const hosted =
|
|
130
131
|
<K extends keyof CelCatalogHandlers>(
|
|
131
132
|
name: K,
|
|
132
|
-
build: (
|
|
133
|
-
handler: CelCatalogHandlers[K],
|
|
134
|
-
) => (args: readonly CelValue[], ctx: CelCallContext) => CelValue,
|
|
133
|
+
build: (handler: CelCatalogHandlers[K]) => CelImplementation,
|
|
135
134
|
): CatalogImplementation =>
|
|
136
135
|
(handlers) => {
|
|
137
136
|
const handler = handlers[name];
|
|
138
137
|
if (handler === undefined) {
|
|
139
|
-
return (
|
|
138
|
+
return (ctx) =>
|
|
140
139
|
celError(
|
|
141
140
|
"unbound_function",
|
|
142
141
|
`${name}() is supplied by the host, and this environment was given no implementation for it`,
|
|
@@ -148,8 +147,10 @@ const hosted =
|
|
|
148
147
|
|
|
149
148
|
// --- reading arguments ------------------------------------------------------
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
// `CelValue | undefined` because a parameter list is as wide as the widest overload; the
|
|
151
|
+
// dispatcher has already resolved on this argument's type.
|
|
152
|
+
const text = (value: CelValue | undefined): string => value as string;
|
|
153
|
+
const bytes = (value: CelValue | undefined): Uint8Array => value as Uint8Array;
|
|
153
154
|
|
|
154
155
|
/**
|
|
155
156
|
* A list argument. Every list-taking entry reads one through here, because a `dyn` slot can
|
|
@@ -157,13 +158,13 @@ const bytes = (value: CelValue): Uint8Array => value as Uint8Array;
|
|
|
157
158
|
* with its host language's own `TypeError` for the rest, and neither is an answer about the
|
|
158
159
|
* argument. Named in the catalog's voice instead, as an instant and a map already are.
|
|
159
160
|
*/
|
|
160
|
-
function list(what: string, value: CelValue): readonly CelValue[] {
|
|
161
|
+
function list(what: string, value: CelValue | undefined): readonly CelValue[] {
|
|
161
162
|
if (Array.isArray(value)) return value;
|
|
162
163
|
throw new CatalogRefusal(`${what}: expected a list, got ${celTypeNameOf(value) ?? "no CEL value"}`);
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
/** A value as a number, the way every arithmetic and formatting entry reads one. */
|
|
166
|
-
function numberOf(value: CelValue): number {
|
|
167
|
+
function numberOf(value: CelValue | undefined): number {
|
|
167
168
|
if (typeof value === "number") return value;
|
|
168
169
|
if (typeof value === "bigint") return Number(value);
|
|
169
170
|
if (isCelUint(value)) return Number(value.value);
|
|
@@ -180,7 +181,7 @@ function numberOf(value: CelValue): number {
|
|
|
180
181
|
* engine being replaced wrote a map as `[object Object]`, which is a value no author
|
|
181
182
|
* ever wrote and which prints into a document.
|
|
182
183
|
*/
|
|
183
|
-
function textOf(what: string, value: CelValue): string {
|
|
184
|
+
function textOf(what: string, value: CelValue | undefined): string {
|
|
184
185
|
if (typeof value === "string") return value;
|
|
185
186
|
if (typeof value === "bigint") return String(value);
|
|
186
187
|
if (isCelUint(value)) return String(value.value);
|
|
@@ -202,7 +203,7 @@ function textOf(what: string, value: CelValue): string {
|
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
/** An instant argument. A `dyn` slot can carry anything, so it is named, never coerced. */
|
|
205
|
-
function instantArg(what: string, value: CelValue): CelTimestamp {
|
|
206
|
+
function instantArg(what: string, value: CelValue | undefined): CelTimestamp {
|
|
206
207
|
if (isCelTimestamp(value)) return value;
|
|
207
208
|
throw new CatalogRefusal(`${what}: expected a timestamp`);
|
|
208
209
|
}
|
|
@@ -222,7 +223,7 @@ function instantOfMillis(what: string, millis: number): CelValue {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
/** A map's entries, whichever container holds them. A list is named rather than joined. */
|
|
225
|
-
function mapEntries(what: string, value: CelValue): readonly (readonly [CelValue, CelValue])[] {
|
|
226
|
+
function mapEntries(what: string, value: CelValue | undefined): readonly (readonly [CelValue, CelValue])[] {
|
|
226
227
|
if (isCelMap(value)) return [...value.entries.values()].map((entry) => [entry.key, entry.value] as const);
|
|
227
228
|
if (Array.isArray(value)) {
|
|
228
229
|
throw new CatalogRefusal(`${what}: expected a map, got a list — use '+' to join lists`);
|
|
@@ -234,9 +235,9 @@ function mapEntries(what: string, value: CelValue): readonly (readonly [CelValue
|
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
/** A map of the same container the entries came out of, so what comes back is what went in. */
|
|
237
|
-
function mapOfEntries(like: CelValue, pairs: readonly (readonly [CelValue, CelValue])[]): CelValue {
|
|
238
|
+
function mapOfEntries(like: CelValue | undefined, pairs: readonly (readonly [CelValue, CelValue])[]): CelValue {
|
|
238
239
|
if (isCelMap(like)) {
|
|
239
|
-
const entries = new Map<
|
|
240
|
+
const entries = new Map<CelMapKey, CelMapValueEntry>();
|
|
240
241
|
for (const [key, value] of pairs) {
|
|
241
242
|
const identity = mapKeyIdentity(key);
|
|
242
243
|
if (identity === undefined) continue;
|
|
@@ -279,7 +280,7 @@ const MAX_EXACT_INT = 9007199254740991n;
|
|
|
279
280
|
* converted here — and past 2^53 a double stops representing every integer, so emitting a
|
|
280
281
|
* number that is not the one the author computed is refused rather than printed.
|
|
281
282
|
*/
|
|
282
|
-
function formattable(what: string, value: CelValue): number {
|
|
283
|
+
function formattable(what: string, value: CelValue | undefined): number {
|
|
283
284
|
if (typeof value === "bigint" || isCelUint(value)) {
|
|
284
285
|
const whole = typeof value === "bigint" ? value : value.value;
|
|
285
286
|
if (whole > MAX_EXACT_INT || whole < -MAX_EXACT_INT) {
|
|
@@ -318,7 +319,7 @@ const FORMAT_TYPES = new Set([..."efgrs%pbodxXcn"]);
|
|
|
318
319
|
*/
|
|
319
320
|
const MAX_DECIMALS = 10;
|
|
320
321
|
|
|
321
|
-
function digitCount(what: string, digits: CelValue): number {
|
|
322
|
+
function digitCount(what: string, digits: CelValue | undefined): number {
|
|
322
323
|
const count = numberOf(digits);
|
|
323
324
|
if (!Number.isInteger(count) || count < 0 || count > MAX_DECIMALS) {
|
|
324
325
|
throw new CatalogRefusal(
|
|
@@ -336,7 +337,7 @@ function digitCount(what: string, digits: CelValue): number {
|
|
|
336
337
|
const FORMATTER_CACHE_CAPACITY = 256;
|
|
337
338
|
const formatters = new BoundedCache<string, (value: number) => string>(FORMATTER_CACHE_CAPACITY);
|
|
338
339
|
|
|
339
|
-
function formatter(what: string, specifier: CelValue): (value: number) => string {
|
|
340
|
+
function formatter(what: string, specifier: CelValue | undefined): (value: number) => string {
|
|
340
341
|
const written = textOf(what, specifier);
|
|
341
342
|
const held = formatters.get(written);
|
|
342
343
|
if (held) return held;
|
|
@@ -361,7 +362,7 @@ function formatter(what: string, specifier: CelValue): (value: number) => string
|
|
|
361
362
|
}
|
|
362
363
|
|
|
363
364
|
/** Render a minute count against a declared day length — a policy, never an assumption. */
|
|
364
|
-
function durationText(minutes: CelValue, minutesPerDay: CelValue): string {
|
|
365
|
+
function durationText(minutes: CelValue | undefined, minutesPerDay: CelValue | undefined): string {
|
|
365
366
|
const perDay = Math.round(formattable("formatDuration", minutesPerDay));
|
|
366
367
|
if (!Number.isFinite(perDay) || perDay <= 0) {
|
|
367
368
|
throw new CatalogRefusal(`formatDuration: minutesPerDay must be a positive number, got ${perDay}`);
|
|
@@ -415,7 +416,7 @@ function re2Flags(what: string, flags: CelValue | undefined): number {
|
|
|
415
416
|
* author. A parse failure outside that closed vocabulary is a defect here, raised as one
|
|
416
417
|
* with the library's message rather than printed as though it were a kind.
|
|
417
418
|
*/
|
|
418
|
-
function pattern(what: string, source: CelValue, flags: CelValue | undefined): RE2JS {
|
|
419
|
+
function pattern(what: string, source: CelValue | undefined, flags: CelValue | undefined): RE2JS {
|
|
419
420
|
const bits = re2Flags(what, flags);
|
|
420
421
|
const written = textOf(what, source);
|
|
421
422
|
return patternOrRefusal(what, written, bits);
|
|
@@ -552,8 +553,8 @@ function distinct(values: readonly CelValue[]): CelValue[] {
|
|
|
552
553
|
}
|
|
553
554
|
|
|
554
555
|
/** Drop entries whose value is null or the empty string. Nothing else. */
|
|
555
|
-
function compacted(value: CelValue): CelValue {
|
|
556
|
-
const keep = (held: CelValue): boolean => held !== null && held !== "";
|
|
556
|
+
function compacted(value: CelValue | undefined): CelValue {
|
|
557
|
+
const keep = (held: CelValue | undefined): boolean => held !== null && held !== "";
|
|
557
558
|
if (Array.isArray(value)) return value.filter(keep);
|
|
558
559
|
return mapOfEntries(
|
|
559
560
|
value,
|
|
@@ -565,23 +566,23 @@ function compacted(value: CelValue): CelValue {
|
|
|
565
566
|
|
|
566
567
|
const IMPLEMENTATIONS = new Map<string, CatalogImplementation>([
|
|
567
568
|
// collections
|
|
568
|
-
["join(list<dyn>, string)", own((
|
|
569
|
-
["keys(map<dyn, dyn>)", own((
|
|
570
|
-
["values(map<dyn, dyn>)", own((
|
|
571
|
-
["distinct(list<dyn>)", own((
|
|
572
|
-
["reverse(list<dyn>)", own((
|
|
573
|
-
["flatten(list<dyn>)", own((
|
|
574
|
-
["sort(list<dyn>)", own((
|
|
569
|
+
["join(list<dyn>, string)", own((ctx, a, b) => list("join", a).map((held) => textOf("join", held)).join(text(b)))],
|
|
570
|
+
["keys(map<dyn, dyn>)", own((ctx, a) => mapEntries("keys", a).map(([key]) => key))],
|
|
571
|
+
["values(map<dyn, dyn>)", own((ctx, a) => mapEntries("values", a).map(([, value]) => value))],
|
|
572
|
+
["distinct(list<dyn>)", own((ctx, a) => distinct(list("distinct", a)))],
|
|
573
|
+
["reverse(list<dyn>)", own((ctx, a) => [...list("reverse", a)].reverse())],
|
|
574
|
+
["flatten(list<dyn>)", own((ctx, a) => (list("flatten", a) as CelValue[]).flat())],
|
|
575
|
+
["sort(list<dyn>)", own((ctx, a) => sorted("sort", list("sort", a)))],
|
|
575
576
|
[
|
|
576
577
|
"range(int)",
|
|
577
|
-
own((
|
|
578
|
-
Array.from({ length: Math.max(0, Math.trunc(numberOf(
|
|
578
|
+
own((ctx, a) =>
|
|
579
|
+
Array.from({ length: Math.max(0, Math.trunc(numberOf(a))) }, (_unused, at) => BigInt(at)),
|
|
579
580
|
),
|
|
580
581
|
],
|
|
581
582
|
[
|
|
582
583
|
"enumerate(list<dyn>)",
|
|
583
|
-
own((
|
|
584
|
-
list("enumerate",
|
|
584
|
+
own((ctx, a) =>
|
|
585
|
+
list("enumerate", a).map((value, at) => {
|
|
585
586
|
const entry = Object.create(null) as Record<string, CelValue>;
|
|
586
587
|
entry.index = BigInt(at);
|
|
587
588
|
entry.value = value;
|
|
@@ -589,117 +590,117 @@ const IMPLEMENTATIONS = new Map<string, CatalogImplementation>([
|
|
|
589
590
|
}),
|
|
590
591
|
),
|
|
591
592
|
],
|
|
592
|
-
["compact(list<dyn>)", own((
|
|
593
|
-
["compact(map<dyn, dyn>)", own((
|
|
593
|
+
["compact(list<dyn>)", own((ctx, a) => compacted(a))],
|
|
594
|
+
["compact(map<dyn, dyn>)", own((ctx, a) => compacted(a))],
|
|
594
595
|
[
|
|
595
596
|
"merge(map<dyn, dyn>, map<dyn, dyn>)",
|
|
596
|
-
own((
|
|
597
|
-
mapOfEntries(
|
|
597
|
+
own((ctx, a, b) =>
|
|
598
|
+
mapOfEntries(a, [...mapEntries("merge", a), ...mapEntries("merge", b)]),
|
|
598
599
|
),
|
|
599
600
|
],
|
|
600
601
|
// One body over the three: the overloads exist so that a statically wrong receiver is a
|
|
601
602
|
// type error, and at runtime a `dyn` receiver reaches whichever of them resolved first, so
|
|
602
603
|
// the VALUE decides what is sliced.
|
|
603
|
-
["slice(bytes, int, int)", own(sliced)],
|
|
604
|
-
["slice(string, int, int)", own(sliced)],
|
|
605
|
-
["slice(list<dyn>, int, int)", own(sliced)],
|
|
604
|
+
["slice(bytes, int, int)", own((ctx, a, b, c) => sliced(a, b, c))],
|
|
605
|
+
["slice(string, int, int)", own((ctx, a, b, c) => sliced(a, b, c))],
|
|
606
|
+
["slice(list<dyn>, int, int)", own((ctx, a, b, c) => sliced(a, b, c))],
|
|
606
607
|
|
|
607
608
|
// strings
|
|
608
|
-
["lower(string)", own((
|
|
609
|
-
["upper(string)", own((
|
|
610
|
-
["trim(string)", own((
|
|
611
|
-
["replace(string, string, string)", own((
|
|
612
|
-
["split(string, string)", own((
|
|
609
|
+
["lower(string)", own((ctx, a) => text(a).toLowerCase())],
|
|
610
|
+
["upper(string)", own((ctx, a) => text(a).toUpperCase())],
|
|
611
|
+
["trim(string)", own((ctx, a) => text(a).trim())],
|
|
612
|
+
["replace(string, string, string)", own((ctx, a, b, c) => text(a).split(text(b)).join(text(c)))],
|
|
613
|
+
["split(string, string)", own((ctx, a, b) => text(a).split(text(b)))],
|
|
613
614
|
[
|
|
614
615
|
"trimPrefix(string, string)",
|
|
615
|
-
own((
|
|
616
|
-
const prefix = text(
|
|
617
|
-
return text(
|
|
616
|
+
own((ctx, a, b) => {
|
|
617
|
+
const prefix = text(b);
|
|
618
|
+
return text(a).startsWith(prefix) ? text(a).slice(prefix.length) : text(a);
|
|
618
619
|
}),
|
|
619
620
|
],
|
|
620
621
|
[
|
|
621
622
|
"trimSuffix(string, string)",
|
|
622
|
-
own((
|
|
623
|
-
const suffix = text(
|
|
624
|
-
const value = text(
|
|
623
|
+
own((ctx, a, b) => {
|
|
624
|
+
const suffix = text(b);
|
|
625
|
+
const value = text(a);
|
|
625
626
|
return suffix !== "" && value.endsWith(suffix) ? value.slice(0, value.length - suffix.length) : value;
|
|
626
627
|
}),
|
|
627
628
|
],
|
|
628
629
|
[
|
|
629
630
|
"regexReplace(string, string, string)",
|
|
630
|
-
own((
|
|
631
|
+
own((ctx, a, b, c) => pattern("regexReplace", b, undefined).matcher(text(a)).replaceAll(text(c))),
|
|
631
632
|
],
|
|
632
633
|
[
|
|
633
634
|
"regexReplace(string, string, string, string)",
|
|
634
|
-
own((
|
|
635
|
+
own((ctx, a, b, c, d) => pattern("regexReplace", b, d).matcher(text(a)).replaceAll(text(c))),
|
|
635
636
|
],
|
|
636
|
-
["regexExtract(string, string)", own((
|
|
637
|
-
["regexExtract(string, string, string)", own((
|
|
638
|
-
["regexExtractAll(string, string)", own((
|
|
639
|
-
["regexExtractAll(string, string, string)", own((
|
|
640
|
-
["regexGroups(string, string)", own((
|
|
641
|
-
["regexGroups(string, string, string)", own((
|
|
637
|
+
["regexExtract(string, string)", own((ctx, a, b) => firstMatch("regexExtract", a, b, undefined))],
|
|
638
|
+
["regexExtract(string, string, string)", own((ctx, a, b, c) => firstMatch("regexExtract", a, b, c))],
|
|
639
|
+
["regexExtractAll(string, string)", own((ctx, a, b) => everyMatch("regexExtractAll", a, b, undefined))],
|
|
640
|
+
["regexExtractAll(string, string, string)", own((ctx, a, b, c) => everyMatch("regexExtractAll", a, b, c))],
|
|
641
|
+
["regexGroups(string, string)", own((ctx, a, b) => matchGroups("regexGroups", a, b, undefined))],
|
|
642
|
+
["regexGroups(string, string, string)", own((ctx, a, b, c) => matchGroups("regexGroups", a, b, c))],
|
|
642
643
|
|
|
643
644
|
// math
|
|
644
|
-
["abs(dyn)", own((
|
|
645
|
-
["floor(dyn)", own((
|
|
646
|
-
["ceil(dyn)", own((
|
|
645
|
+
["abs(dyn)", own((ctx, a) => Math.abs(numberOf(a)))],
|
|
646
|
+
["floor(dyn)", own((ctx, a) => Math.floor(numberOf(a)))],
|
|
647
|
+
["ceil(dyn)", own((ctx, a) => Math.ceil(numberOf(a)))],
|
|
647
648
|
// Both arities go through `formattable`, so the 2^53 refusal does not depend on which
|
|
648
649
|
// one the author wrote: guarding only the two-argument form left `round(x)` silently
|
|
649
650
|
// answering with a neighbouring integer, reachable by writing one fewer argument.
|
|
650
|
-
["round(double)", own((
|
|
651
|
-
["round(int)", own((
|
|
652
|
-
["round(double, int)", own((
|
|
653
|
-
["round(int, int)", own((
|
|
654
|
-
["min(list<dyn>)", own((
|
|
655
|
-
["max(list<dyn>)", own((
|
|
651
|
+
["round(double)", own((ctx, a) => Math.round(formattable("round", a)))],
|
|
652
|
+
["round(int)", own((ctx, a) => Math.round(formattable("round", a)))],
|
|
653
|
+
["round(double, int)", own((ctx, a, b) => roundedTo(a, b))],
|
|
654
|
+
["round(int, int)", own((ctx, a, b) => roundedTo(a, b))],
|
|
655
|
+
["min(list<dyn>)", own((ctx, a) => smallest(list("min", a), true))],
|
|
656
|
+
["max(list<dyn>)", own((ctx, a) => smallest(list("max", a), false))],
|
|
656
657
|
[
|
|
657
658
|
"sum(list<dyn>)",
|
|
658
|
-
own((
|
|
659
|
+
own((ctx, a) => list("sum", a).reduce((total: number, held) => total + numberOf(held), 0)),
|
|
659
660
|
],
|
|
660
661
|
[
|
|
661
662
|
"avg(list<dyn>)",
|
|
662
|
-
own((
|
|
663
|
-
const values = list("avg",
|
|
663
|
+
own((ctx, a) => {
|
|
664
|
+
const values = list("avg", a);
|
|
664
665
|
if (values.length === 0) return null;
|
|
665
666
|
return values.reduce((total: number, held) => total + numberOf(held), 0) / values.length;
|
|
666
667
|
}),
|
|
667
668
|
],
|
|
668
669
|
|
|
669
670
|
// JSON
|
|
670
|
-
["json(dyn)", hosted("json", (handler) => (
|
|
671
|
-
["parseJson(string)", own((
|
|
671
|
+
["json(dyn)", hosted("json", (handler) => (ctx, a) => handler(a as CelValue))],
|
|
672
|
+
["parseJson(string)", own((ctx, a) => parseJsonText(text(a)))],
|
|
672
673
|
|
|
673
674
|
// paths
|
|
674
675
|
[
|
|
675
676
|
"string.joinPath(string)",
|
|
676
|
-
hosted("joinPath", (handler) => (
|
|
677
|
+
hosted("joinPath", (handler) => (ctx, a, b) => handler(text(a), text(b))),
|
|
677
678
|
],
|
|
678
679
|
|
|
679
680
|
// encoding
|
|
680
|
-
["base64Encode(string)", hosted("base64Encode", (handler) => (
|
|
681
|
-
["base64Decode(string)", hosted("base64Decode", (handler) => (
|
|
682
|
-
["bytesFromBase64(string)", own((
|
|
683
|
-
["bytesToBase64(bytes)", own((
|
|
684
|
-
["urlEncode(string)", own((
|
|
685
|
-
["urlDecode(string)", own((
|
|
681
|
+
["base64Encode(string)", hosted("base64Encode", (handler) => (ctx, a) => handler(text(a)))],
|
|
682
|
+
["base64Decode(string)", hosted("base64Decode", (handler) => (ctx, a) => handler(text(a)))],
|
|
683
|
+
["bytesFromBase64(string)", own((ctx, a) => bytesFromBase64(text(a)))],
|
|
684
|
+
["bytesToBase64(bytes)", own((ctx, a) => base64Text(bytes(a)))],
|
|
685
|
+
["urlEncode(string)", own((ctx, a) => encodeURIComponent(text(a)))],
|
|
686
|
+
["urlDecode(string)", own((ctx, a) => decodeURIComponent(text(a)))],
|
|
686
687
|
|
|
687
688
|
// hashing
|
|
688
|
-
["sha256(string)", hosted("sha256", (handler) => (
|
|
689
|
-
["md5(string)", hosted("md5", (handler) => (
|
|
690
|
-
["sha1(string)", hosted("sha1", (handler) => (
|
|
691
|
-
["sha512(string)", hosted("sha512", (handler) => (
|
|
689
|
+
["sha256(string)", hosted("sha256", (handler) => (ctx, a) => handler(text(a)))],
|
|
690
|
+
["md5(string)", hosted("md5", (handler) => (ctx, a) => handler(text(a)))],
|
|
691
|
+
["sha1(string)", hosted("sha1", (handler) => (ctx, a) => handler(text(a)))],
|
|
692
|
+
["sha512(string)", hosted("sha512", (handler) => (ctx, a) => handler(text(a)))],
|
|
692
693
|
[
|
|
693
694
|
"hmac(string, string, string)",
|
|
694
|
-
hosted("hmac", (handler) => (
|
|
695
|
+
hosted("hmac", (handler) => (ctx, a, b, c) => handler(text(a), text(b), text(c))),
|
|
695
696
|
],
|
|
696
697
|
|
|
697
698
|
// null handling
|
|
698
|
-
["default(dyn, dyn)", own((
|
|
699
|
+
["default(dyn, dyn)", own((ctx, a, b) => (a === null ? (b as CelValue) : (a as CelValue)))],
|
|
699
700
|
[
|
|
700
701
|
"coalesce(list<dyn>)",
|
|
701
|
-
own((
|
|
702
|
-
const found = list("coalesce",
|
|
702
|
+
own((ctx, a) => {
|
|
703
|
+
const found = list("coalesce", a).find((held) => held !== null);
|
|
703
704
|
return found === undefined ? null : found;
|
|
704
705
|
}),
|
|
705
706
|
],
|
|
@@ -707,37 +708,37 @@ const IMPLEMENTATIONS = new Map<string, CatalogImplementation>([
|
|
|
707
708
|
// time
|
|
708
709
|
["now()", own(() => instantOfMillis("now", Date.now()))],
|
|
709
710
|
["nowIso()", own(() => isoTextIn(Date.now(), "UTC"))],
|
|
710
|
-
["nowIso(string)", own((
|
|
711
|
+
["nowIso(string)", own((ctx, a) => isoTextIn(Date.now(), zoneArg("nowIso", a)))],
|
|
711
712
|
["today()", own(() => dateTextIn(Date.now(), "UTC"))],
|
|
712
|
-
["today(string)", own((
|
|
713
|
+
["today(string)", own((ctx, a) => dateTextIn(Date.now(), zoneArg("today", a)))],
|
|
713
714
|
["nowMillis()", own(() => BigInt(Date.now()))],
|
|
714
715
|
["nowSeconds()", own(() => BigInt(Math.floor(Date.now() / 1000)))],
|
|
715
|
-
["dateIn(google.protobuf.Timestamp)", own((
|
|
716
|
-
["dateIn(google.protobuf.Timestamp, string)", own((
|
|
717
|
-
["isoIn(google.protobuf.Timestamp)", own((
|
|
718
|
-
["isoIn(google.protobuf.Timestamp, string)", own((
|
|
719
|
-
["startOfMonth(google.protobuf.Timestamp)", own((
|
|
720
|
-
["startOfMonth(google.protobuf.Timestamp, string)", own((
|
|
721
|
-
["addMonths(google.protobuf.Timestamp, int)", own((
|
|
722
|
-
["addMonths(google.protobuf.Timestamp, int, string)", own((
|
|
716
|
+
["dateIn(google.protobuf.Timestamp)", own((ctx, a) => dateIn(a, undefined))],
|
|
717
|
+
["dateIn(google.protobuf.Timestamp, string)", own((ctx, a, b) => dateIn(a, b))],
|
|
718
|
+
["isoIn(google.protobuf.Timestamp)", own((ctx, a) => isoIn(a, undefined))],
|
|
719
|
+
["isoIn(google.protobuf.Timestamp, string)", own((ctx, a, b) => isoIn(a, b))],
|
|
720
|
+
["startOfMonth(google.protobuf.Timestamp)", own((ctx, a) => startOfMonth(a, undefined))],
|
|
721
|
+
["startOfMonth(google.protobuf.Timestamp, string)", own((ctx, a, b) => startOfMonth(a, b))],
|
|
722
|
+
["addMonths(google.protobuf.Timestamp, int)", own((ctx, a, b) => addMonths(a, b, undefined))],
|
|
723
|
+
["addMonths(google.protobuf.Timestamp, int, string)", own((ctx, a, b, c) => addMonths(a, b, c))],
|
|
723
724
|
|
|
724
725
|
// formatting
|
|
725
|
-
["format(double, string)", own((
|
|
726
|
-
["format(int, string)", own((
|
|
727
|
-
["fixed(double, int)", own((
|
|
728
|
-
["fixed(int, int)", own((
|
|
729
|
-
["formatDuration(double, int)", own((
|
|
730
|
-
["formatDuration(int, int)", own((
|
|
726
|
+
["format(double, string)", own((ctx, a, b) => formatter("format", b)(formattable("format", a)))],
|
|
727
|
+
["format(int, string)", own((ctx, a, b) => formatter("format", b)(formattable("format", a)))],
|
|
728
|
+
["fixed(double, int)", own((ctx, a, b) => fixedText(a, b))],
|
|
729
|
+
["fixed(int, int)", own((ctx, a, b) => fixedText(a, b))],
|
|
730
|
+
["formatDuration(double, int)", own((ctx, a, b) => durationText(a, b))],
|
|
731
|
+
["formatDuration(int, int)", own((ctx, a, b) => durationText(a, b))],
|
|
731
732
|
|
|
732
733
|
// uuid
|
|
733
734
|
["uuidv1()", own(() => v1())],
|
|
734
735
|
["uuidv4()", own(() => v4())],
|
|
735
736
|
["uuidv6()", own(() => v6())],
|
|
736
737
|
["uuidv7()", own(() => v7())],
|
|
737
|
-
["uuidv3(string, string)", own((
|
|
738
|
-
["uuidv5(string, string)", own((
|
|
739
|
-
["uuidValidate(string)", own((
|
|
740
|
-
["uuidVersion(string)", own((
|
|
738
|
+
["uuidv3(string, string)", own((ctx, a, b) => v3(text(a), text(b)))],
|
|
739
|
+
["uuidv5(string, string)", own((ctx, a, b) => v5(text(a), text(b)))],
|
|
740
|
+
["uuidValidate(string)", own((ctx, a) => uuidValidate(text(a)))],
|
|
741
|
+
["uuidVersion(string)", own((ctx, a) => BigInt(uuidVersion(text(a))))],
|
|
741
742
|
]);
|
|
742
743
|
|
|
743
744
|
/**
|
|
@@ -745,10 +746,9 @@ const IMPLEMENTATIONS = new Map<string, CatalogImplementation>([
|
|
|
745
746
|
* which neither `String.slice` nor `TypedArray.subarray` accepts. `subarray` rather than
|
|
746
747
|
* `slice` for bytes: a view costs no copy, and every consumer treats the result as read-only.
|
|
747
748
|
*/
|
|
748
|
-
function sliced(
|
|
749
|
-
const from = Number(
|
|
750
|
-
const to = Number(
|
|
751
|
-
const value = args[0]!;
|
|
749
|
+
function sliced(value: CelValue | undefined, start: CelValue | undefined, end: CelValue | undefined): CelValue {
|
|
750
|
+
const from = Number(start as bigint);
|
|
751
|
+
const to = Number(end as bigint);
|
|
752
752
|
if (typeof value === "string") return value.slice(from, to);
|
|
753
753
|
if (isCelBytes(value)) return value.subarray(from, to);
|
|
754
754
|
if (Array.isArray(value)) return value.slice(from, to);
|
|
@@ -757,53 +757,53 @@ function sliced(args: readonly CelValue[]): CelValue {
|
|
|
757
757
|
);
|
|
758
758
|
}
|
|
759
759
|
|
|
760
|
-
function roundedTo(
|
|
761
|
-
return Number(formattable("round",
|
|
760
|
+
function roundedTo(value: CelValue | undefined, digits: CelValue | undefined): number {
|
|
761
|
+
return Number(formattable("round", value).toFixed(digitCount("round", digits)));
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
-
function fixedText(
|
|
765
|
-
return formatter("fixed", `.${digitCount("fixed",
|
|
764
|
+
function fixedText(value: CelValue | undefined, digits: CelValue | undefined): string {
|
|
765
|
+
return formatter("fixed", `.${digitCount("fixed", digits)}f`)(formattable("fixed", value));
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
-
function firstMatch(what: string,
|
|
769
|
-
const matcher = pattern(what,
|
|
768
|
+
function firstMatch(what: string, subject: CelValue | undefined, source: CelValue | undefined, flags: CelValue | undefined): string {
|
|
769
|
+
const matcher = pattern(what, source, flags).matcher(text(subject));
|
|
770
770
|
return matcher.find() ? (matcher.group() ?? "") : "";
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
-
function everyMatch(what: string,
|
|
774
|
-
const matcher = pattern(what,
|
|
773
|
+
function everyMatch(what: string, subject: CelValue | undefined, source: CelValue | undefined, flags: CelValue | undefined): string[] {
|
|
774
|
+
const matcher = pattern(what, source, flags).matcher(text(subject));
|
|
775
775
|
const out: string[] = [];
|
|
776
776
|
while (matcher.find()) out.push(matcher.group() ?? "");
|
|
777
777
|
return out;
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
-
function matchGroups(what: string,
|
|
781
|
-
const matcher = pattern(what,
|
|
780
|
+
function matchGroups(what: string, subject: CelValue | undefined, source: CelValue | undefined, flags: CelValue | undefined): string[] {
|
|
781
|
+
const matcher = pattern(what, source, flags).matcher(text(subject));
|
|
782
782
|
if (!matcher.find()) return [];
|
|
783
783
|
return Array.from({ length: matcher.groupCount() }, (_unused, at) => matcher.group(at + 1) ?? "");
|
|
784
784
|
}
|
|
785
785
|
|
|
786
|
-
function dateIn(
|
|
787
|
-
return dateTextIn(millisOf(instantArg("dateIn",
|
|
786
|
+
function dateIn(instant: CelValue | undefined, zone: CelValue | undefined): string {
|
|
787
|
+
return dateTextIn(millisOf(instantArg("dateIn", instant)), zoneArg("dateIn", zone));
|
|
788
788
|
}
|
|
789
789
|
|
|
790
|
-
function isoIn(
|
|
791
|
-
return isoTextIn(millisOf(instantArg("isoIn",
|
|
790
|
+
function isoIn(instant: CelValue | undefined, zone: CelValue | undefined): string {
|
|
791
|
+
return isoTextIn(millisOf(instantArg("isoIn", instant)), zoneArg("isoIn", zone));
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
-
function startOfMonth(
|
|
794
|
+
function startOfMonth(instant: CelValue | undefined, zone: CelValue | undefined): CelValue {
|
|
795
795
|
const named = zoneArg("startOfMonth", zone);
|
|
796
|
-
const fields = civilTimeIn(millisOf(instantArg("startOfMonth",
|
|
796
|
+
const fields = civilTimeIn(millisOf(instantArg("startOfMonth", instant)), named);
|
|
797
797
|
return instantOfMillis(
|
|
798
798
|
"startOfMonth",
|
|
799
799
|
instantOfCivilTime({ ...fields, day: 1, hour: 0, minute: 0, second: 0 }, named),
|
|
800
800
|
);
|
|
801
801
|
}
|
|
802
802
|
|
|
803
|
-
function addMonths(
|
|
803
|
+
function addMonths(instant: CelValue | undefined, months: CelValue | undefined, zone: CelValue | undefined): CelValue {
|
|
804
804
|
const named = zoneArg("addMonths", zone);
|
|
805
|
-
const fields = civilTimeIn(millisOf(instantArg("addMonths",
|
|
806
|
-
const shifted = fields.year * 12 + (fields.month - 1) + Number(
|
|
805
|
+
const fields = civilTimeIn(millisOf(instantArg("addMonths", instant)), named);
|
|
806
|
+
const shifted = fields.year * 12 + (fields.month - 1) + Number(months as bigint);
|
|
807
807
|
// `%` takes the dividend's sign in JS, so a negative total would yield month 0.
|
|
808
808
|
const year = Math.floor(shifted / 12);
|
|
809
809
|
const month = (((shifted % 12) + 12) % 12) + 1;
|
package/src/cel-map-value.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Building a CEL map, and the one identity a key has.
|
|
3
3
|
*
|
|
4
|
-
* A map's entries live in a `Map` keyed by
|
|
5
|
-
*
|
|
4
|
+
* A map's entries live in a `Map` keyed by each key's **own typed value**, never by a
|
|
5
|
+
* property name. Three consequences, all load-bearing:
|
|
6
6
|
*
|
|
7
7
|
* - A map holds CEL's four key types (int, uint, bool, string) in one container, and an
|
|
8
8
|
* int key and a uint key of the same number are **one** key, because CEL equality
|
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
* - Nothing a key spells can reach a prototype, a method or an inherited member. A map
|
|
11
11
|
* built from `{'__proto__': 1}` holds `__proto__` as data and reads it back as data;
|
|
12
12
|
* dropping such a key would be swallowing what the author wrote.
|
|
13
|
+
* - The types separate themselves. A `Map` compares its keys by type as well as value, so
|
|
14
|
+
* a string `"1"` and an int `1` are distinct keys with nothing prefixed to say so — and a
|
|
15
|
+
* second engine keys an entry by its own discriminant rather than reproducing this one's
|
|
16
|
+
* text byte for byte.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
import type { CelMap, CelMapValueEntry, CelValue } from "./cel-value.js";
|
|
19
|
+
import type { CelMap, CelMapKey, CelMapValueEntry, CelValue } from "./cel-value.js";
|
|
16
20
|
import {
|
|
17
21
|
CEL_VALUE_TYPE,
|
|
18
22
|
celError,
|
|
@@ -23,36 +27,42 @@ import {
|
|
|
23
27
|
import type { SourceRange } from "./syntax-tree.js";
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
|
-
*
|
|
30
|
+
* What identifies a key. The `bigint` covers the numeric types together: CEL equality
|
|
27
31
|
* makes `1`, `1u` and `1.0` one key, so a map carrying two of them is not a map and a
|
|
28
32
|
* lookup by any of the three finds the entry — which is what
|
|
29
33
|
* `{1u: 1.0, 2: 2.0, 3u: 3.0}[?3.0]` reads. A double that is not whole identifies
|
|
30
34
|
* nothing, since no entry can hold it.
|
|
35
|
+
*
|
|
36
|
+
* A string and a bool identify themselves, and nothing is prefixed to keep the types
|
|
37
|
+
* apart: a `Map` key of one type never equals a key of another, so `"1"` and `1` are
|
|
38
|
+
* distinct keys and `"true"` is not `true`.
|
|
31
39
|
*/
|
|
32
|
-
export function mapKeyIdentity(key: CelValue):
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
if (typeof key === "number" && Number.isInteger(key)) return `n${BigInt(key)}`;
|
|
38
|
-
return undefined;
|
|
40
|
+
export function mapKeyIdentity(key: CelValue): CelMapKey | undefined {
|
|
41
|
+
const held = typeof key;
|
|
42
|
+
if (held === "string" || held === "boolean" || held === "bigint") return key as CelMapKey;
|
|
43
|
+
if (held === "number") return Number.isInteger(key) ? BigInt(key as number) : undefined;
|
|
44
|
+
return isCelUint(key) ? key.value : undefined;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
/** An empty map, for a literal with no entries. */
|
|
42
|
-
export function celMapOf(entries: ReadonlyMap<
|
|
48
|
+
export function celMapOf(entries: ReadonlyMap<CelMapKey, CelMapValueEntry>): CelMap {
|
|
43
49
|
return { [CEL_VALUE_TYPE]: "map", entries };
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
53
|
* A map from its entries in written order, or the error that stops it: a key of a type
|
|
48
54
|
* no map is keyed by, or two keys CEL equality makes one.
|
|
55
|
+
*
|
|
56
|
+
* The entries arrive **flat** — key, value, key, value — because a literal of n entries
|
|
57
|
+
* then costs one allocation rather than one per entry, and both backends write the same
|
|
58
|
+
* call. A duplicate is caught by the size not moving, which is the one hash lookup the
|
|
59
|
+
* insert already performs.
|
|
49
60
|
*/
|
|
50
|
-
export function celMapFromEntries(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (const [key, value] of pairs) {
|
|
61
|
+
export function celMapFromEntries(flat: readonly CelValue[], range?: SourceRange): CelMap | CelError {
|
|
62
|
+
const entries = new Map<CelMapKey, CelMapValueEntry>();
|
|
63
|
+
for (let at = 0; at < flat.length; at += 2) {
|
|
64
|
+
const key = flat[at] as CelValue;
|
|
65
|
+
const value = flat[at + 1] as CelValue;
|
|
56
66
|
if (isCelError(key)) return key;
|
|
57
67
|
if (isCelError(value)) return value;
|
|
58
68
|
// A map is **built** with an int, uint, bool or string key: a double is not a key
|
|
@@ -66,10 +76,11 @@ export function celMapFromEntries(
|
|
|
66
76
|
range,
|
|
67
77
|
);
|
|
68
78
|
}
|
|
69
|
-
|
|
79
|
+
const before = entries.size;
|
|
80
|
+
entries.set(identity, { key, value });
|
|
81
|
+
if (entries.size === before) {
|
|
70
82
|
return celError("duplicate_map_key", `the key ${describeKey(key)} is written twice`, range);
|
|
71
83
|
}
|
|
72
|
-
entries.set(identity, { key, value });
|
|
73
84
|
}
|
|
74
85
|
return celMapOf(entries);
|
|
75
86
|
}
|