@telorun/cel 0.107.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/backend-runtime.d.ts +46 -8
  2. package/dist/backend-runtime.d.ts.map +1 -1
  3. package/dist/backend-runtime.js +133 -30
  4. package/dist/catalog-runtime.d.ts.map +1 -1
  5. package/dist/catalog-runtime.js +110 -109
  6. package/dist/cel-map-value.d.ts +20 -7
  7. package/dist/cel-map-value.d.ts.map +1 -1
  8. package/dist/cel-map-value.js +29 -18
  9. package/dist/cel-value.d.ts +10 -3
  10. package/dist/cel-value.d.ts.map +1 -1
  11. package/dist/check-diagnostic.d.ts +3 -2
  12. package/dist/check-diagnostic.d.ts.map +1 -1
  13. package/dist/checker.d.ts +9 -0
  14. package/dist/checker.d.ts.map +1 -1
  15. package/dist/checker.js +98 -13
  16. package/dist/closure-backend.js +66 -15
  17. package/dist/duration-value.d.ts +11 -0
  18. package/dist/duration-value.d.ts.map +1 -1
  19. package/dist/duration-value.js +17 -3
  20. package/dist/emitted-module.d.ts +1 -1
  21. package/dist/emitted-module.js +1 -1
  22. package/dist/engine-version.d.ts +1 -1
  23. package/dist/engine-version.js +1 -1
  24. package/dist/environment.d.ts +8 -0
  25. package/dist/environment.d.ts.map +1 -1
  26. package/dist/environment.js +20 -5
  27. package/dist/function-registry.d.ts +40 -0
  28. package/dist/function-registry.d.ts.map +1 -1
  29. package/dist/function-registry.js +105 -2
  30. package/dist/index.d.ts +10 -8
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +12 -6
  33. package/dist/js-emitter.d.ts +3 -2
  34. package/dist/js-emitter.d.ts.map +1 -1
  35. package/dist/js-emitter.js +11 -5
  36. package/dist/macro-check.d.ts +8 -0
  37. package/dist/macro-check.d.ts.map +1 -1
  38. package/dist/macro-check.js +1 -1
  39. package/dist/member-read.d.ts +42 -5
  40. package/dist/member-read.d.ts.map +1 -1
  41. package/dist/member-read.js +47 -6
  42. package/dist/resolved-call.d.ts +13 -0
  43. package/dist/resolved-call.d.ts.map +1 -1
  44. package/dist/runtime-library.d.ts +13 -2
  45. package/dist/runtime-library.d.ts.map +1 -1
  46. package/dist/runtime-library.js +134 -125
  47. package/dist/serializer.d.ts.map +1 -1
  48. package/dist/serializer.js +20 -4
  49. package/dist/timestamp-value.d.ts +9 -0
  50. package/dist/timestamp-value.d.ts.map +1 -1
  51. package/dist/timestamp-value.js +15 -0
  52. package/dist/type-expression.d.ts +18 -2
  53. package/dist/type-expression.d.ts.map +1 -1
  54. package/dist/type-expression.js +23 -3
  55. package/dist/value-equality.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/backend-runtime.ts +180 -30
  58. package/src/catalog-runtime.ts +133 -133
  59. package/src/cel-map-value.ts +31 -20
  60. package/src/cel-value.ts +11 -3
  61. package/src/check-diagnostic.ts +4 -2
  62. package/src/checker.ts +123 -10
  63. package/src/closure-backend.ts +59 -14
  64. package/src/duration-value.ts +18 -3
  65. package/src/emitted-module.ts +1 -1
  66. package/src/engine-version.ts +1 -1
  67. package/src/environment.ts +26 -5
  68. package/src/function-registry.ts +114 -2
  69. package/src/index.ts +24 -3
  70. package/src/js-emitter.ts +13 -5
  71. package/src/macro-check.ts +7 -1
  72. package/src/member-read.ts +48 -7
  73. package/src/resolved-call.ts +13 -0
  74. package/src/runtime-library.ts +167 -143
  75. package/src/serializer.ts +19 -3
  76. package/src/timestamp-value.ts +16 -0
  77. package/src/type-expression.ts +25 -3
  78. package/src/value-equality.ts +3 -3
@@ -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: (args: readonly CelValue[], ctx: CelCallContext) => CelValue): CelImplementation {
108
- return (args, ctx) => {
108
+ function refusing(run: CelImplementation): CelImplementation {
109
+ return (ctx, a, b, c, d) => {
109
110
  try {
110
- return run(args, ctx);
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: (args: readonly CelValue[], ctx: CelCallContext) => CelValue): CatalogImplementation =>
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 (_args, ctx) =>
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
- const text = (value: CelValue): string => value as string;
152
- const bytes = (value: CelValue): Uint8Array => value as Uint8Array;
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<string, CelMapValueEntry>();
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((args) => list("join", args[0]!).map((held) => textOf("join", held)).join(text(args[1]!)))],
569
- ["keys(map<dyn, dyn>)", own((args) => mapEntries("keys", args[0]!).map(([key]) => key))],
570
- ["values(map<dyn, dyn>)", own((args) => mapEntries("values", args[0]!).map(([, value]) => value))],
571
- ["distinct(list<dyn>)", own((args) => distinct(list("distinct", args[0]!)))],
572
- ["reverse(list<dyn>)", own((args) => [...list("reverse", args[0]!)].reverse())],
573
- ["flatten(list<dyn>)", own((args) => (list("flatten", args[0]!) as CelValue[]).flat())],
574
- ["sort(list<dyn>)", own((args) => sorted("sort", list("sort", args[0]!)))],
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((args) =>
578
- Array.from({ length: Math.max(0, Math.trunc(numberOf(args[0]!))) }, (_unused, at) => BigInt(at)),
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((args) =>
584
- list("enumerate", args[0]!).map((value, at) => {
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((args) => compacted(args[0]!))],
593
- ["compact(map<dyn, dyn>)", own((args) => compacted(args[0]!))],
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((args) =>
597
- mapOfEntries(args[0]!, [...mapEntries("merge", args[0]!), ...mapEntries("merge", args[1]!)]),
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((args) => text(args[0]!).toLowerCase())],
609
- ["upper(string)", own((args) => text(args[0]!).toUpperCase())],
610
- ["trim(string)", own((args) => text(args[0]!).trim())],
611
- ["replace(string, string, string)", own((args) => text(args[0]!).split(text(args[1]!)).join(text(args[2]!)))],
612
- ["split(string, string)", own((args) => text(args[0]!).split(text(args[1]!)))],
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((args) => {
616
- const prefix = text(args[1]!);
617
- return text(args[0]!).startsWith(prefix) ? text(args[0]!).slice(prefix.length) : args[0]!;
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((args) => {
623
- const suffix = text(args[1]!);
624
- const value = text(args[0]!);
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((args) => pattern("regexReplace", args[1]!, undefined).matcher(text(args[0]!)).replaceAll(text(args[2]!))),
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((args) => pattern("regexReplace", args[1]!, args[3]!).matcher(text(args[0]!)).replaceAll(text(args[2]!))),
635
+ own((ctx, a, b, c, d) => pattern("regexReplace", b, d).matcher(text(a)).replaceAll(text(c))),
635
636
  ],
636
- ["regexExtract(string, string)", own((args) => firstMatch("regexExtract", args, undefined))],
637
- ["regexExtract(string, string, string)", own((args) => firstMatch("regexExtract", args, args[2]!))],
638
- ["regexExtractAll(string, string)", own((args) => everyMatch("regexExtractAll", args, undefined))],
639
- ["regexExtractAll(string, string, string)", own((args) => everyMatch("regexExtractAll", args, args[2]!))],
640
- ["regexGroups(string, string)", own((args) => matchGroups("regexGroups", args, undefined))],
641
- ["regexGroups(string, string, string)", own((args) => matchGroups("regexGroups", args, args[2]!))],
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((args) => Math.abs(numberOf(args[0]!)))],
645
- ["floor(dyn)", own((args) => Math.floor(numberOf(args[0]!)))],
646
- ["ceil(dyn)", own((args) => Math.ceil(numberOf(args[0]!)))],
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((args) => Math.round(formattable("round", args[0]!)))],
651
- ["round(int)", own((args) => Math.round(formattable("round", args[0]!)))],
652
- ["round(double, int)", own((args) => roundedTo(args))],
653
- ["round(int, int)", own((args) => roundedTo(args))],
654
- ["min(list<dyn>)", own((args) => smallest(list("min", args[0]!), true))],
655
- ["max(list<dyn>)", own((args) => smallest(list("max", args[0]!), false))],
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((args) => list("sum", args[0]!).reduce((total: number, held) => total + numberOf(held), 0)),
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((args) => {
663
- const values = list("avg", args[0]!);
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) => (args) => handler(args[0]!))],
671
- ["parseJson(string)", own((args) => parseJsonText(text(args[0]!)))],
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) => (args) => handler(text(args[0]!), text(args[1]!))),
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) => (args) => handler(text(args[0]!)))],
681
- ["base64Decode(string)", hosted("base64Decode", (handler) => (args) => handler(text(args[0]!)))],
682
- ["bytesFromBase64(string)", own((args) => bytesFromBase64(text(args[0]!)))],
683
- ["bytesToBase64(bytes)", own((args) => base64Text(bytes(args[0]!)))],
684
- ["urlEncode(string)", own((args) => encodeURIComponent(text(args[0]!)))],
685
- ["urlDecode(string)", own((args) => decodeURIComponent(text(args[0]!)))],
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) => (args) => handler(text(args[0]!)))],
689
- ["md5(string)", hosted("md5", (handler) => (args) => handler(text(args[0]!)))],
690
- ["sha1(string)", hosted("sha1", (handler) => (args) => handler(text(args[0]!)))],
691
- ["sha512(string)", hosted("sha512", (handler) => (args) => handler(text(args[0]!)))],
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) => (args) => handler(text(args[0]!), text(args[1]!), text(args[2]!))),
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((args) => (args[0] === null ? args[1]! : args[0]!))],
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((args) => {
702
- const found = list("coalesce", args[0]!).find((held) => held !== null);
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((args) => isoTextIn(Date.now(), zoneArg("nowIso", args[0]!)))],
711
+ ["nowIso(string)", own((ctx, a) => isoTextIn(Date.now(), zoneArg("nowIso", a)))],
711
712
  ["today()", own(() => dateTextIn(Date.now(), "UTC"))],
712
- ["today(string)", own((args) => dateTextIn(Date.now(), zoneArg("today", args[0]!)))],
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((args) => dateIn(args, undefined))],
716
- ["dateIn(google.protobuf.Timestamp, string)", own((args) => dateIn(args, args[1]!))],
717
- ["isoIn(google.protobuf.Timestamp)", own((args) => isoIn(args, undefined))],
718
- ["isoIn(google.protobuf.Timestamp, string)", own((args) => isoIn(args, args[1]!))],
719
- ["startOfMonth(google.protobuf.Timestamp)", own((args) => startOfMonth(args, undefined))],
720
- ["startOfMonth(google.protobuf.Timestamp, string)", own((args) => startOfMonth(args, args[1]!))],
721
- ["addMonths(google.protobuf.Timestamp, int)", own((args) => addMonths(args, undefined))],
722
- ["addMonths(google.protobuf.Timestamp, int, string)", own((args) => addMonths(args, args[2]!))],
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((args) => formatter("format", args[1]!)(formattable("format", args[0]!)))],
726
- ["format(int, string)", own((args) => formatter("format", args[1]!)(formattable("format", args[0]!)))],
727
- ["fixed(double, int)", own((args) => fixedText(args))],
728
- ["fixed(int, int)", own((args) => fixedText(args))],
729
- ["formatDuration(double, int)", own((args) => durationText(args[0]!, args[1]!))],
730
- ["formatDuration(int, int)", own((args) => durationText(args[0]!, args[1]!))],
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((args) => v3(text(args[0]!), text(args[1]!)))],
738
- ["uuidv5(string, string)", own((args) => v5(text(args[0]!), text(args[1]!)))],
739
- ["uuidValidate(string)", own((args) => uuidValidate(text(args[0]!)))],
740
- ["uuidVersion(string)", own((args) => BigInt(uuidVersion(text(args[0]!))))],
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(args: readonly CelValue[]): CelValue {
749
- const from = Number(args[1] as bigint);
750
- const to = Number(args[2] as bigint);
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(args: readonly CelValue[]): number {
761
- return Number(formattable("round", args[0]!).toFixed(digitCount("round", args[1]!)));
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(args: readonly CelValue[]): string {
765
- return formatter("fixed", `.${digitCount("fixed", args[1]!)}f`)(formattable("fixed", args[0]!));
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, args: readonly CelValue[], flags: CelValue | undefined): string {
769
- const matcher = pattern(what, args[1]!, flags).matcher(text(args[0]!));
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, args: readonly CelValue[], flags: CelValue | undefined): string[] {
774
- const matcher = pattern(what, args[1]!, flags).matcher(text(args[0]!));
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, args: readonly CelValue[], flags: CelValue | undefined): string[] {
781
- const matcher = pattern(what, args[1]!, flags).matcher(text(args[0]!));
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(args: readonly CelValue[], zone: CelValue | undefined): string {
787
- return dateTextIn(millisOf(instantArg("dateIn", args[0]!)), zoneArg("dateIn", zone));
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(args: readonly CelValue[], zone: CelValue | undefined): string {
791
- return isoTextIn(millisOf(instantArg("isoIn", args[0]!)), zoneArg("isoIn", zone));
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(args: readonly CelValue[], zone: CelValue | undefined): CelValue {
794
+ function startOfMonth(instant: CelValue | undefined, zone: CelValue | undefined): CelValue {
795
795
  const named = zoneArg("startOfMonth", zone);
796
- const fields = civilTimeIn(millisOf(instantArg("startOfMonth", args[0]!)), named);
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(args: readonly CelValue[], zone: CelValue | undefined): CelValue {
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", args[0]!)), named);
806
- const shifted = fields.year * 12 + (fields.month - 1) + Number(args[1] as bigint);
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;
@@ -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 the **canonical text** of each key, never by
5
- * a property name. Two consequences, both load-bearing:
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
- * The text that identifies a key. `n` covers the numeric types together: CEL equality
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): string | undefined {
33
- if (typeof key === "string") return `s${key}`;
34
- if (typeof key === "boolean") return `b${key}`;
35
- if (typeof key === "bigint") return `n${key}`;
36
- if (isCelUint(key)) return `n${key.value}`;
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<string, CelMapValueEntry>): CelMap {
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
- pairs: readonly (readonly [CelValue, CelValue])[],
52
- range?: SourceRange,
53
- ): CelMap | CelError {
54
- const entries = new Map<string, CelMapValueEntry>();
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
- if (entries.has(identity)) {
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
  }