@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
@@ -78,19 +78,42 @@ export interface CelCallContext {
78
78
  readonly range?: SourceRange;
79
79
  }
80
80
 
81
- export type CelImplementation = (args: readonly CelValue[], ctx: CelCallContext) => CelValue;
81
+ /**
82
+ * What an implementation IS: its call context, then its arguments **positionally**.
83
+ *
84
+ * The dispatch key fixes each overload's arity, so there is never a count to carry and
85
+ * never an array to build — a call's cost is the call. `CALL_SITE_DIRECT_ARITY` is the
86
+ * bound, and it is the widest arity any registration declares; a trailing argument an
87
+ * overload does not take is `undefined`, which no CEL value ever is, so an absent one is
88
+ * unambiguous. In a port the same contract is a context plus a slice.
89
+ */
90
+ export type CelImplementation = (
91
+ ctx: CelCallContext,
92
+ a?: CelValue,
93
+ b?: CelValue,
94
+ c?: CelValue,
95
+ d?: CelValue,
96
+ ) => CelValue;
97
+
98
+ /** The widest arity a registration declares, and so the number of direct parameters. */
99
+ export const CALL_SITE_DIRECT_ARITY = 4;
82
100
 
83
101
  // --- reading arguments the dispatcher has already typed ---------------------
84
102
 
85
- const text = (value: CelValue): string => value as string;
86
- const integer = (value: CelValue): bigint => value as bigint;
87
- const unsigned = (value: CelValue): bigint => (value as { value: bigint }).value;
88
- const double = (value: CelValue): number => value as number;
89
- const bytes = (value: CelValue): Uint8Array => value as Uint8Array;
90
- const list = (value: CelValue): readonly CelValue[] => value as readonly CelValue[];
91
- const instant = (value: CelValue): CelTimestamp => value as CelTimestamp;
92
- const span = (value: CelValue): CelDuration => value as CelDuration;
93
- const optional = (value: CelValue): CelOptional => value as CelOptional;
103
+ // Each takes `CelValue | undefined` because a parameter list is as wide as the widest
104
+ // overload; the dispatcher has already resolved on this argument's type, so the cast is
105
+ // what reading it means.
106
+ const text = (value: CelValue | undefined): string => value as string;
107
+ const integer = (value: CelValue | undefined): bigint => value as bigint;
108
+ const unsigned = (value: CelValue | undefined): bigint => (value as { value: bigint }).value;
109
+ const double = (value: CelValue | undefined): number => value as number;
110
+ const bytes = (value: CelValue | undefined): Uint8Array => value as Uint8Array;
111
+ const list = (value: CelValue | undefined): readonly CelValue[] => value as readonly CelValue[];
112
+ const instant = (value: CelValue | undefined): CelTimestamp => value as CelTimestamp;
113
+ const span = (value: CelValue | undefined): CelDuration => value as CelDuration;
114
+ const optional = (value: CelValue | undefined): CelOptional => value as CelOptional;
115
+ /** The argument itself, where what is read of it is that the dispatcher resolved on it. */
116
+ const value = (held: CelValue | undefined): CelValue => held as CelValue;
94
117
 
95
118
  /** The code points of text — CEL counts and indexes a string by character. */
96
119
  const characters = (value: string): string[] => [...value];
@@ -250,122 +273,123 @@ function splitText(value: string, separator: string, limit?: bigint): CelValue {
250
273
  // --- the table -------------------------------------------------------------
251
274
 
252
275
  function timestampGetter(field: TimestampField): CelImplementation {
253
- return (args, ctx) =>
254
- timestampField(instant(args[0]!), field, args.length > 1 ? text(args[1]!) : undefined, ctx.range);
276
+ // One implementation under both arities: the zone is absent where the call omits it, and
277
+ // no CEL value is `undefined`, so the absence is the argument's own answer.
278
+ return (ctx, a, b) => timestampField(instant(a), field, b === undefined ? undefined : text(b), ctx.range);
255
279
  }
256
280
 
257
281
  function durationGetter(field: DurationField): CelImplementation {
258
- return (args) => durationField(span(args[0]!), field);
282
+ return (ctx, a) => durationField(span(a), field);
259
283
  }
260
284
 
261
285
  const IMPLEMENTATIONS = new Map<string, CelImplementation>([
262
286
  // conversions
263
- ["bool(bool)", (args) => args[0]!],
287
+ ["bool(bool)", (ctx, a) => value(a)],
264
288
  [
265
289
  "bool(string)",
266
- (args, ctx) => {
267
- const held = BOOL_WORDS[text(args[0]!)];
268
- return held === undefined ? conversionError("bool()", JSON.stringify(args[0]), ctx.range) : held;
290
+ (ctx, a) => {
291
+ const held = BOOL_WORDS[text(a)];
292
+ return held === undefined ? conversionError("bool()", JSON.stringify(a), ctx.range) : held;
269
293
  },
270
294
  ],
271
- ["bytes(bytes)", (args) => args[0]!],
272
- ["bytes(string)", (args) => textToBytes(text(args[0]!))],
273
- ["double(double)", (args) => args[0]!],
274
- ["double(int)", (args) => Number(integer(args[0]!))],
275
- ["double(uint)", (args) => Number(unsigned(args[0]!))],
276
- ["double(string)", (args, ctx) => doubleFromText(text(args[0]!), ctx.range)],
277
- ["duration(string)", (args, ctx) => parseDuration(text(args[0]!), ctx.range)],
278
- ["duration(google.protobuf.Duration)", (args) => args[0]!],
279
- ["dyn(A)", (args) => args[0]!],
280
- ["int(int)", (args) => args[0]!],
281
- ["int(uint)", (args, ctx) => intResult(unsigned(args[0]!), ctx.range)],
282
- ["int(double)", (args, ctx) => intFromDouble(double(args[0]!), ctx.range)],
295
+ ["bytes(bytes)", (ctx, a) => value(a)],
296
+ ["bytes(string)", (ctx, a) => textToBytes(text(a))],
297
+ ["double(double)", (ctx, a) => value(a)],
298
+ ["double(int)", (ctx, a) => Number(integer(a))],
299
+ ["double(uint)", (ctx, a) => Number(unsigned(a))],
300
+ ["double(string)", (ctx, a) => doubleFromText(text(a), ctx.range)],
301
+ ["duration(string)", (ctx, a) => parseDuration(text(a), ctx.range)],
302
+ ["duration(google.protobuf.Duration)", (ctx, a) => value(a)],
303
+ ["dyn(A)", (ctx, a) => value(a)],
304
+ ["int(int)", (ctx, a) => value(a)],
305
+ ["int(uint)", (ctx, a) => intResult(unsigned(a), ctx.range)],
306
+ ["int(double)", (ctx, a) => intFromDouble(double(a), ctx.range)],
283
307
  [
284
308
  "int(string)",
285
- (args, ctx) => {
286
- const written = text(args[0]!);
309
+ (ctx, a) => {
310
+ const written = text(a);
287
311
  if (!DECIMAL_INT.test(written)) return conversionError("int()", JSON.stringify(written), ctx.range);
288
312
  return intResult(BigInt(written), ctx.range);
289
313
  },
290
314
  ],
291
- ["int(google.protobuf.Timestamp)", (args) => instant(args[0]!).seconds],
315
+ ["int(google.protobuf.Timestamp)", (ctx, a) => instant(a).seconds],
292
316
  // A duration's whole seconds, truncated toward zero — no row pins it, cel-spec does.
293
- ["int(google.protobuf.Duration)", (args) => durationField(span(args[0]!), "getSeconds")],
294
- ["matches(string, string)", (args, ctx) => celMatches(text(args[0]!), text(args[1]!), ctx.range)],
295
- ["size(string)", (args) => BigInt(characters(text(args[0]!)).length)],
296
- ["size(bytes)", (args) => BigInt(bytes(args[0]!).length)],
297
- ["size(list<A>)", (args) => BigInt(list(args[0]!).length)],
298
- ["size(map<K, V>)", (args) => BigInt(sizeOfMap(args[0]!))],
299
- ["string(string)", (args) => args[0]!],
300
- ["string(bool)", (args) => String(args[0]!)],
301
- ["string(int)", (args) => String(integer(args[0]!))],
302
- ["string(uint)", (args) => String(unsigned(args[0]!))],
303
- ["string(double)", (args) => doubleText(double(args[0]!))],
304
- ["string(bytes)", (args, ctx) => bytesToText(bytes(args[0]!), ctx.range)],
305
- ["string(google.protobuf.Timestamp)", (args) => formatTimestamp(instant(args[0]!))],
306
- ["string(google.protobuf.Duration)", (args) => formatDuration(span(args[0]!))],
307
- ["timestamp(string)", (args, ctx) => parseTimestamp(text(args[0]!), ctx.range)],
308
- ["timestamp(int)", (args, ctx) => celTimestamp(integer(args[0]!), 0, ctx.range)],
309
- ["timestamp(google.protobuf.Timestamp)", (args) => args[0]!],
310
- ["uint(uint)", (args) => args[0]!],
311
- ["uint(int)", (args, ctx) => uintResult(integer(args[0]!), ctx.range)],
312
- ["uint(double)", (args, ctx) => uintFromDouble(double(args[0]!), ctx.range)],
317
+ ["int(google.protobuf.Duration)", (ctx, a) => durationField(span(a), "getSeconds")],
318
+ ["matches(string, string)", (ctx, a, b) => celMatches(text(a), text(b), ctx.range)],
319
+ ["size(string)", (ctx, a) => BigInt(characters(text(a)).length)],
320
+ ["size(bytes)", (ctx, a) => BigInt(bytes(a).length)],
321
+ ["size(list<A>)", (ctx, a) => BigInt(list(a).length)],
322
+ ["size(map<K, V>)", (ctx, a) => BigInt(sizeOfMap(a))],
323
+ ["string(string)", (ctx, a) => value(a)],
324
+ ["string(bool)", (ctx, a) => String(a)],
325
+ ["string(int)", (ctx, a) => String(integer(a))],
326
+ ["string(uint)", (ctx, a) => String(unsigned(a))],
327
+ ["string(double)", (ctx, a) => doubleText(double(a))],
328
+ ["string(bytes)", (ctx, a) => bytesToText(bytes(a), ctx.range)],
329
+ ["string(google.protobuf.Timestamp)", (ctx, a) => formatTimestamp(instant(a))],
330
+ ["string(google.protobuf.Duration)", (ctx, a) => formatDuration(span(a))],
331
+ ["timestamp(string)", (ctx, a) => parseTimestamp(text(a), ctx.range)],
332
+ ["timestamp(int)", (ctx, a) => celTimestamp(integer(a), 0, ctx.range)],
333
+ ["timestamp(google.protobuf.Timestamp)", (ctx, a) => value(a)],
334
+ ["uint(uint)", (ctx, a) => value(a)],
335
+ ["uint(int)", (ctx, a) => uintResult(integer(a), ctx.range)],
336
+ ["uint(double)", (ctx, a) => uintFromDouble(double(a), ctx.range)],
313
337
  [
314
338
  "uint(string)",
315
- (args, ctx) => {
316
- const written = text(args[0]!);
339
+ (ctx, a) => {
340
+ const written = text(a);
317
341
  if (!DECIMAL_UINT.test(written)) return conversionError("uint()", JSON.stringify(written), ctx.range);
318
342
  return uintResult(BigInt(written), ctx.range);
319
343
  },
320
344
  ],
321
345
 
322
346
  // string members
323
- ["string.size()", (args) => BigInt(characters(text(args[0]!)).length)],
324
- ["bytes.size()", (args) => BigInt(bytes(args[0]!).length)],
325
- ["list<A>.size()", (args) => BigInt(list(args[0]!).length)],
326
- ["map<K, V>.size()", (args) => BigInt(sizeOfMap(args[0]!))],
327
- ["string.startsWith(string)", (args) => text(args[0]!).startsWith(text(args[1]!))],
328
- ["string.endsWith(string)", (args) => text(args[0]!).endsWith(text(args[1]!))],
329
- ["string.contains(string)", (args) => text(args[0]!).includes(text(args[1]!))],
330
- ["string.matches(string)", (args, ctx) => celMatches(text(args[0]!), text(args[1]!), ctx.range)],
347
+ ["string.size()", (ctx, a) => BigInt(characters(text(a)).length)],
348
+ ["bytes.size()", (ctx, a) => BigInt(bytes(a).length)],
349
+ ["list<A>.size()", (ctx, a) => BigInt(list(a).length)],
350
+ ["map<K, V>.size()", (ctx, a) => BigInt(sizeOfMap(a))],
351
+ ["string.startsWith(string)", (ctx, a, b) => text(a).startsWith(text(b))],
352
+ ["string.endsWith(string)", (ctx, a, b) => text(a).endsWith(text(b))],
353
+ ["string.contains(string)", (ctx, a, b) => text(a).includes(text(b))],
354
+ ["string.matches(string)", (ctx, a, b) => celMatches(text(a), text(b), ctx.range)],
331
355
  // Named for ASCII and not limited to it — see the declaration's own `spec: false` reason.
332
- ["string.lowerAscii()", (args) => text(args[0]!).toLowerCase()],
333
- ["string.upperAscii()", (args) => text(args[0]!).toUpperCase()],
334
- ["string.trim()", (args) => text(args[0]!).trim()],
335
- ["string.indexOf(string)", (args) => BigInt(text(args[0]!).indexOf(text(args[1]!)))],
356
+ ["string.lowerAscii()", (ctx, a) => text(a).toLowerCase()],
357
+ ["string.upperAscii()", (ctx, a) => text(a).toUpperCase()],
358
+ ["string.trim()", (ctx, a) => text(a).trim()],
359
+ ["string.indexOf(string)", (ctx, a, b) => BigInt(text(a).indexOf(text(b)))],
336
360
  [
337
361
  "string.indexOf(string, int)",
338
- (args, ctx) => {
339
- const from = boundedIndex(integer(args[2]!), text(args[0]!).length, ctx.range);
340
- return typeof from === "number" ? BigInt(text(args[0]!).indexOf(text(args[1]!), from)) : from;
362
+ (ctx, a, b, c) => {
363
+ const from = boundedIndex(integer(c), text(a).length, ctx.range);
364
+ return typeof from === "number" ? BigInt(text(a).indexOf(text(b), from)) : from;
341
365
  },
342
366
  ],
343
- ["string.lastIndexOf(string)", (args) => BigInt(text(args[0]!).lastIndexOf(text(args[1]!)))],
367
+ ["string.lastIndexOf(string)", (ctx, a, b) => BigInt(text(a).lastIndexOf(text(b)))],
344
368
  [
345
369
  "string.lastIndexOf(string, int)",
346
- (args, ctx) => {
347
- const from = boundedIndex(integer(args[2]!), text(args[0]!).length, ctx.range);
348
- return typeof from === "number" ? BigInt(text(args[0]!).lastIndexOf(text(args[1]!), from)) : from;
370
+ (ctx, a, b, c) => {
371
+ const from = boundedIndex(integer(c), text(a).length, ctx.range);
372
+ return typeof from === "number" ? BigInt(text(a).lastIndexOf(text(b), from)) : from;
349
373
  },
350
374
  ],
351
- ["string.substring(int)", (args, ctx) => substring(text(args[0]!), integer(args[1]!), undefined, ctx.range)],
375
+ ["string.substring(int)", (ctx, a, b) => substring(text(a), integer(b), undefined, ctx.range)],
352
376
  [
353
377
  "string.substring(int, int)",
354
- (args, ctx) => substring(text(args[0]!), integer(args[1]!), integer(args[2]!), ctx.range),
378
+ (ctx, a, b, c) => substring(text(a), integer(b), integer(c), ctx.range),
355
379
  ],
356
- ["string.split(string)", (args) => splitText(text(args[0]!), text(args[1]!))],
357
- ["string.split(string, int)", (args) => splitText(text(args[0]!), text(args[1]!), integer(args[2]!))],
358
- ["list<string>.join()", (args) => list(args[0]!).join("")],
359
- ["list<string>.join(string)", (args) => list(args[0]!).join(text(args[1]!))],
380
+ ["string.split(string)", (ctx, a, b) => splitText(text(a), text(b))],
381
+ ["string.split(string, int)", (ctx, a, b, c) => splitText(text(a), text(b), integer(c))],
382
+ ["list<string>.join()", (ctx, a) => list(a).join("")],
383
+ ["list<string>.join(string)", (ctx, a, b) => list(a).join(text(b))],
360
384
 
361
385
  // bytes members, each a compatibility member rather than CEL's
362
- ["bytes.string()", (args, ctx) => bytesToText(bytes(args[0]!), ctx.range)],
363
- ["bytes.hex()", (args) => hexText(bytes(args[0]!))],
364
- ["bytes.base64()", (args) => base64Text(bytes(args[0]!))],
386
+ ["bytes.string()", (ctx, a) => bytesToText(bytes(a), ctx.range)],
387
+ ["bytes.hex()", (ctx, a) => hexText(bytes(a))],
388
+ ["bytes.base64()", (ctx, a) => base64Text(bytes(a))],
365
389
  [
366
390
  "bytes.json()",
367
- (args, ctx) => {
368
- const decoded = bytesToText(bytes(args[0]!), ctx.range);
391
+ (ctx, a) => {
392
+ const decoded = bytesToText(bytes(a), ctx.range);
369
393
  if (typeof decoded !== "string") return decoded;
370
394
  try {
371
395
  return jsonAsCelValue(JSON.parse(decoded));
@@ -376,9 +400,9 @@ const IMPLEMENTATIONS = new Map<string, CelImplementation>([
376
400
  ],
377
401
  [
378
402
  "bytes.at(int)",
379
- (args, ctx) => {
380
- const held = bytes(args[0]!);
381
- const at = integer(args[1]!);
403
+ (ctx, a, b) => {
404
+ const held = bytes(a);
405
+ const at = integer(b);
382
406
  if (at < 0n || at >= BigInt(held.length)) {
383
407
  return celError("index_out_of_range", `index out of range: ${at}`, ctx.range);
384
408
  }
@@ -387,99 +411,99 @@ const IMPLEMENTATIONS = new Map<string, CelImplementation>([
387
411
  ],
388
412
 
389
413
  // arithmetic
390
- ["!(bool)", (args) => !(args[0] as boolean)],
391
- ["-(int)", (args, ctx) => intResult(-integer(args[0]!), ctx.range)],
392
- ["-(double)", (args) => -double(args[0]!)],
393
- ["+(int, int)", (args, ctx) => intResult(integer(args[0]!) + integer(args[1]!), ctx.range)],
394
- ["+(uint, uint)", (args, ctx) => uintResult(unsigned(args[0]!) + unsigned(args[1]!), ctx.range)],
395
- ["+(double, double)", (args) => double(args[0]!) + double(args[1]!)],
396
- ["+(string, string)", (args) => text(args[0]!) + text(args[1]!)],
414
+ ["!(bool)", (ctx, a) => !(a as boolean)],
415
+ ["-(int)", (ctx, a) => intResult(-integer(a), ctx.range)],
416
+ ["-(double)", (ctx, a) => -double(a)],
417
+ ["+(int, int)", (ctx, a, b) => intResult(integer(a) + integer(b), ctx.range)],
418
+ ["+(uint, uint)", (ctx, a, b) => uintResult(unsigned(a) + unsigned(b), ctx.range)],
419
+ ["+(double, double)", (ctx, a, b) => double(a) + double(b)],
420
+ ["+(string, string)", (ctx, a, b) => text(a) + text(b)],
397
421
  [
398
422
  "+(bytes, bytes)",
399
- (args) => {
400
- const left = bytes(args[0]!);
401
- const right = bytes(args[1]!);
423
+ (ctx, a, b) => {
424
+ const left = bytes(a);
425
+ const right = bytes(b);
402
426
  const out = new Uint8Array(left.length + right.length);
403
427
  out.set(left);
404
428
  out.set(right, left.length);
405
429
  return out;
406
430
  },
407
431
  ],
408
- ["+(list<A>, list<A>)", (args) => [...list(args[0]!), ...list(args[1]!)]],
432
+ ["+(list<A>, list<A>)", (ctx, a, b) => [...list(a), ...list(b)]],
409
433
  [
410
434
  "+(google.protobuf.Timestamp, google.protobuf.Duration)",
411
- (args, ctx) =>
412
- durationOutOfRange(span(args[1]!), ctx.range) ??
413
- celTimestamp(0n, timestampNanos(instant(args[0]!)) + durationNanos(span(args[1]!)), ctx.range),
435
+ (ctx, a, b) =>
436
+ durationOutOfRange(span(b), ctx.range) ??
437
+ celTimestamp(0n, timestampNanos(instant(a)) + durationNanos(span(b)), ctx.range),
414
438
  ],
415
439
  [
416
440
  "+(google.protobuf.Duration, google.protobuf.Timestamp)",
417
- (args, ctx) =>
418
- durationOutOfRange(span(args[0]!), ctx.range) ??
419
- celTimestamp(0n, durationNanos(span(args[0]!)) + timestampNanos(instant(args[1]!)), ctx.range),
441
+ (ctx, a, b) =>
442
+ durationOutOfRange(span(a), ctx.range) ??
443
+ celTimestamp(0n, durationNanos(span(a)) + timestampNanos(instant(b)), ctx.range),
420
444
  ],
421
445
  [
422
446
  "+(google.protobuf.Duration, google.protobuf.Duration)",
423
- (args, ctx) => celDurationFromNanos(durationNanos(span(args[0]!)) + durationNanos(span(args[1]!)), ctx.range),
447
+ (ctx, a, b) => celDurationFromNanos(durationNanos(span(a)) + durationNanos(span(b)), ctx.range),
424
448
  ],
425
- ["-(int, int)", (args, ctx) => intResult(integer(args[0]!) - integer(args[1]!), ctx.range)],
426
- ["-(uint, uint)", (args, ctx) => uintResult(unsigned(args[0]!) - unsigned(args[1]!), ctx.range)],
427
- ["-(double, double)", (args) => double(args[0]!) - double(args[1]!)],
449
+ ["-(int, int)", (ctx, a, b) => intResult(integer(a) - integer(b), ctx.range)],
450
+ ["-(uint, uint)", (ctx, a, b) => uintResult(unsigned(a) - unsigned(b), ctx.range)],
451
+ ["-(double, double)", (ctx, a, b) => double(a) - double(b)],
428
452
  [
429
453
  "-(google.protobuf.Timestamp, google.protobuf.Timestamp)",
430
- (args, ctx) =>
431
- celDurationFromNanos(timestampNanos(instant(args[0]!)) - timestampNanos(instant(args[1]!)), ctx.range),
454
+ (ctx, a, b) =>
455
+ celDurationFromNanos(timestampNanos(instant(a)) - timestampNanos(instant(b)), ctx.range),
432
456
  ],
433
457
  [
434
458
  "-(google.protobuf.Timestamp, google.protobuf.Duration)",
435
- (args, ctx) =>
436
- durationOutOfRange(span(args[1]!), ctx.range) ??
437
- celTimestamp(0n, timestampNanos(instant(args[0]!)) - durationNanos(span(args[1]!)), ctx.range),
459
+ (ctx, a, b) =>
460
+ durationOutOfRange(span(b), ctx.range) ??
461
+ celTimestamp(0n, timestampNanos(instant(a)) - durationNanos(span(b)), ctx.range),
438
462
  ],
439
463
  [
440
464
  "-(google.protobuf.Duration, google.protobuf.Duration)",
441
- (args, ctx) => celDurationFromNanos(durationNanos(span(args[0]!)) - durationNanos(span(args[1]!)), ctx.range),
465
+ (ctx, a, b) => celDurationFromNanos(durationNanos(span(a)) - durationNanos(span(b)), ctx.range),
442
466
  ],
443
- ["*(int, int)", (args, ctx) => intResult(integer(args[0]!) * integer(args[1]!), ctx.range)],
444
- ["*(uint, uint)", (args, ctx) => uintResult(unsigned(args[0]!) * unsigned(args[1]!), ctx.range)],
445
- ["*(double, double)", (args) => double(args[0]!) * double(args[1]!)],
446
- ["/(int, int)", (args, ctx) => intDivide(integer(args[0]!), integer(args[1]!), ctx.range)],
447
- ["/(uint, uint)", (args, ctx) => uintDivide(unsigned(args[0]!), unsigned(args[1]!), ctx.range)],
467
+ ["*(int, int)", (ctx, a, b) => intResult(integer(a) * integer(b), ctx.range)],
468
+ ["*(uint, uint)", (ctx, a, b) => uintResult(unsigned(a) * unsigned(b), ctx.range)],
469
+ ["*(double, double)", (ctx, a, b) => double(a) * double(b)],
470
+ ["/(int, int)", (ctx, a, b) => intDivide(integer(a), integer(b), ctx.range)],
471
+ ["/(uint, uint)", (ctx, a, b) => uintDivide(unsigned(a), unsigned(b), ctx.range)],
448
472
  // A double divided by zero is an infinity, as IEEE 754 says; only the integers refuse.
449
- ["/(double, double)", (args) => double(args[0]!) / double(args[1]!)],
450
- ["%(int, int)", (args, ctx) => intModulo(integer(args[0]!), integer(args[1]!), ctx.range)],
451
- ["%(uint, uint)", (args, ctx) => uintModulo(unsigned(args[0]!), unsigned(args[1]!), ctx.range)],
473
+ ["/(double, double)", (ctx, a, b) => double(a) / double(b)],
474
+ ["%(int, int)", (ctx, a, b) => intModulo(integer(a), integer(b), ctx.range)],
475
+ ["%(uint, uint)", (ctx, a, b) => uintModulo(unsigned(a), unsigned(b), ctx.range)],
452
476
  [
453
477
  "in(A, list<A>)",
454
- (args) => {
478
+ (ctx, a, b) => {
455
479
  // Membership reads every element, so an element that must be awaited is a door too, and
456
480
  // the refusal is TERMINAL as it is in a comprehension: answering `true` off the one
457
481
  // element that is readable decides the question against a list this engine cannot read.
458
- for (const held of list(args[1]!)) {
482
+ for (const held of list(b)) {
459
483
  const refused = asyncValueRefused(held);
460
484
  if (refused) return refused;
461
- if (celEqual(held, args[0]!)) return true;
485
+ if (celEqual(held, value(a))) return true;
462
486
  }
463
487
  return false;
464
488
  },
465
489
  ],
466
- ["in(K, map<K, V>)", (args) => typeof celLookup(args[1]!, args[0]!) !== "symbol"],
490
+ ["in(K, map<K, V>)", (ctx, a, b) => typeof celLookup(value(b), value(a)) !== "symbol"],
467
491
 
468
492
  // the optional library
469
- ["optional<A>.hasValue()", (args) => optional(args[0]!).present],
493
+ ["optional<A>.hasValue()", (ctx, a) => optional(a).present],
470
494
  [
471
495
  "optional<A>.value()",
472
- (args, ctx) => {
473
- const held = optional(args[0]!);
496
+ (ctx, a) => {
497
+ const held = optional(a);
474
498
  return held.present
475
499
  ? (held.held as CelValue)
476
500
  : celError("optional_value_missing", "the optional holds no value", ctx.range);
477
501
  },
478
502
  ],
479
- ["optional<A>.or(optional<A>)", (args) => (optional(args[0]!).present ? args[0]! : args[1]!)],
503
+ ["optional<A>.or(optional<A>)", (ctx, a, b) => (optional(a).present ? value(a) : value(b))],
480
504
  [
481
505
  "optional<A>.orValue(A)",
482
- (args) => (optional(args[0]!).present ? (optional(args[0]!).held as CelValue) : args[1]!),
506
+ (ctx, a, b) => (optional(a).present ? (optional(a).held as CelValue) : value(b)),
483
507
  ],
484
508
 
485
509
  // timestamp and duration fields
@@ -509,7 +533,7 @@ const IMPLEMENTATIONS = new Map<string, CelImplementation>([
509
533
  ["google.protobuf.Duration.getMilliseconds()", durationGetter("getMilliseconds")],
510
534
  ]);
511
535
 
512
- function sizeOfMap(value: CelValue): number {
536
+ function sizeOfMap(value: CelValue | undefined): number {
513
537
  if (isCelMap(value)) return (value as CelMap).entries.size;
514
538
  if (isCelRecord(value)) return Object.keys(value).length;
515
539
  return 0;
@@ -522,25 +546,25 @@ function sizeOfMap(value: CelValue): number {
522
546
  * would then have no implementation.
523
547
  */
524
548
  const BY_NAME = new Map<string, CelImplementation>([
525
- ["==", (args) => celEqual(args[0]!, args[1]!)],
526
- ["!=", (args) => !celEqual(args[0]!, args[1]!)],
549
+ ["==", (ctx, a, b) => celEqual(value(a), value(b))],
550
+ ["!=", (ctx, a, b) => !celEqual(value(a), value(b))],
527
551
  ["<", ordering((compared) => compared < 0)],
528
552
  ["<=", ordering((compared) => compared <= 0)],
529
553
  [">", ordering((compared) => compared > 0)],
530
554
  [">=", ordering((compared) => compared >= 0)],
531
- ["type", (args, ctx) => celTypeValueOf(args[0]!, ctx.range)],
555
+ ["type", (ctx, a) => celTypeValueOf(value(a), ctx.range)],
532
556
  ]);
533
557
 
534
558
  function ordering(holds: (compared: number) => boolean): CelImplementation {
535
- return (args, ctx) => {
536
- const compared = celCompare(args[0]!, args[1]!);
559
+ return (ctx, a, b) => {
560
+ const compared = celCompare(value(a), value(b));
537
561
  // Values that do not order at all — unrelated types, or a NaN. NaN orders with
538
562
  // nothing, including itself, so every comparison against it is false.
539
563
  if (compared === undefined) {
540
- if (isNotANumber(args[0]!) || isNotANumber(args[1]!)) return false;
564
+ if (isNotANumber(a) || isNotANumber(b)) return false;
541
565
  return celError(
542
566
  "no_matching_overload",
543
- `${celTypeNameOf(args[0]!) ?? "this value"} and ${celTypeNameOf(args[1]!) ?? "this value"} do not order`,
567
+ `${celTypeNameOf(a) ?? "this value"} and ${celTypeNameOf(b) ?? "this value"} do not order`,
544
568
  ctx.range,
545
569
  );
546
570
  }
@@ -548,7 +572,7 @@ function ordering(holds: (compared: number) => boolean): CelImplementation {
548
572
  };
549
573
  }
550
574
 
551
- function isNotANumber(value: CelValue): boolean {
575
+ function isNotANumber(value: CelValue | undefined): boolean {
552
576
  return typeof value === "number" && Number.isNaN(value);
553
577
  }
554
578
 
@@ -590,8 +614,8 @@ export function standardConstantValues(optionalTypes: boolean): ReadonlyMap<stri
590
614
  * `optional.ofNonZeroValue(v)`: present unless the value is its type's zero — which is
591
615
  * what makes it different from `optional.of`, and what three of the vectors' rows read.
592
616
  */
593
- export function optionalOfNonZero(value: CelValue): CelOptional {
594
- return isZeroValue(value) ? celNone() : celSome(value);
617
+ export function optionalOfNonZero(held: CelValue): CelOptional {
618
+ return isZeroValue(held) ? celNone() : celSome(held);
595
619
  }
596
620
 
597
621
  /**
@@ -601,7 +625,7 @@ export function optionalOfNonZero(value: CelValue): CelOptional {
601
625
  * engine that picked the epoch as a timestamp's zero would make
602
626
  * `optional.ofNonZeroValue(timestamp(0))` absent, which no row and no rule asks for.
603
627
  */
604
- function isZeroValue(value: CelValue): boolean {
628
+ function isZeroValue(value: CelValue | undefined): boolean {
605
629
  if (value === null || value === false || value === "") return true;
606
630
  if (typeof value === "bigint") return value === 0n;
607
631
  if (typeof value === "number") return value === 0;
package/src/serializer.ts CHANGED
@@ -128,19 +128,35 @@ function writeDouble(value: number): string {
128
128
  return `${sign}${/[.e]/.test(text) ? text : `${text}.0`}`;
129
129
  }
130
130
 
131
+ /**
132
+ * A string literal, always in double quotes.
133
+ *
134
+ * **One spelling, chosen rather than remembered.** The tree records a string's VALUE and not
135
+ * the quote the author typed — the source's own formatting is not structure, which is the
136
+ * same rule that makes the serializer drop redundant parentheses — so a round trip
137
+ * normalizes `'x'` to `"x"`. Both are CEL and the parser reads either.
138
+ *
139
+ * The cost lands on a quick fix, which is written by rewriting the tree and serializing it
140
+ * back **into a YAML scalar**: a double-quoted CEL literal inside a double-quoted scalar has
141
+ * to be escaped, so a repair reads `!cel "a.startsWith(\"x\")"` where the author wrote
142
+ * `'x'`. Choosing the quote here cannot fix that — a single-quoted YAML scalar wants the
143
+ * opposite choice, and this engine does not know which scalar the text will land in. Picking
144
+ * the YAML quoting that needs no escaping is the EDITOR's, which has both halves in hand.
145
+ */
131
146
  function writeString(value: string): string {
132
- let text = '"';
147
+ const quote = '"';
148
+ let text = quote;
133
149
  for (const unit of value) {
134
150
  const code = unit.codePointAt(0)!;
135
151
  if (unit === "\\") text += "\\\\";
136
- else if (unit === '"') text += '\\"';
152
+ else if (unit === quote) text += `\\${quote}`;
137
153
  else if (unit === "\n") text += "\\n";
138
154
  else if (unit === "\r") text += "\\r";
139
155
  else if (unit === "\t") text += "\\t";
140
156
  else if (code < 0x20 || code === 0x7f) text += `\\x${code.toString(16).padStart(2, "0")}`;
141
157
  else text += unit;
142
158
  }
143
- return `${text}"`;
159
+ return `${text}${quote}`;
144
160
  }
145
161
 
146
162
  function writeBytes(value: Uint8Array): string {
@@ -47,6 +47,22 @@ export function celTimestamp(
47
47
  return { [CEL_VALUE_TYPE]: "google.protobuf.Timestamp", seconds: whole, nanos: Number(rest) };
48
48
  }
49
49
 
50
+ /**
51
+ * An instant from epoch MILLISECONDS — the mirror of `celDurationFromNanos` for a host
52
+ * clock reading (`Date.now()`, a driver's epoch-millis column), or the range error.
53
+ *
54
+ * It exists so that a host never has to reach for its own date type to say "now": the
55
+ * instance type is seconds plus nanos, and a millisecond reading is the one other shape
56
+ * a host actually holds. A fractional reading keeps its sub-millisecond part.
57
+ */
58
+ export function celTimestampFromMillis(millis: number, range?: SourceRange): CelTimestamp | CelError {
59
+ if (!Number.isFinite(millis)) {
60
+ return celError("invalid_conversion", "timestamp out of range", range);
61
+ }
62
+ const whole = Math.floor(millis / 1000);
63
+ return celTimestamp(BigInt(whole), Math.round((millis - whole * 1000) * 1_000_000), range);
64
+ }
65
+
50
66
  /** The instant's nanoseconds since the epoch — what arithmetic and ordering compare. */
51
67
  export function timestampNanos(value: CelTimestamp): bigint {
52
68
  return value.seconds * NANOS_PER_SECOND + BigInt(value.nanos);
@@ -9,8 +9,10 @@
9
9
  *
10
10
  * A bare capital letter is a type **parameter**, which is how a signature says "the
11
11
  * same type here and there". A name the host registered as a nominal type resolves
12
- * to it; any other name is refused rather than guessed, because a misspelled type in
13
- * a signature would otherwise register a function nothing can call.
12
+ * to it; any other name is refused rather than guessed — as a
13
+ * {@link CelUnknownTypeNameError}, which a caller holding the name's source may turn
14
+ * into a verdict — because a misspelled type in a signature would otherwise register a
15
+ * function nothing can call.
14
16
  */
15
17
 
16
18
  import type { CelType, PrimitiveName } from "./cel-type.js";
@@ -40,6 +42,26 @@ export class CelTypeExpressionError extends Error {
40
42
  }
41
43
  }
42
44
 
45
+ /**
46
+ * A well-formed type expression naming a type nothing is registered under.
47
+ *
48
+ * It is **not** a `CelTypeExpressionError`, and the split is the whole point: that error
49
+ * is about text this grammar cannot read, while this one is about a name the HOST and its
50
+ * own registry disagree about. A caller that holds where the name was written can turn it
51
+ * into a ranged verdict instead of a crash — which is what a namespace declaration built
52
+ * out of a host's data does — and a caller that cannot still lets it fly.
53
+ */
54
+ export class CelUnknownTypeNameError extends Error {
55
+ /** The name nothing is registered under. */
56
+ readonly typeName: string;
57
+
58
+ constructor(typeName: string) {
59
+ super(`no type is registered under the name ${JSON.stringify(typeName)}`);
60
+ this.name = "CelUnknownTypeNameError";
61
+ this.typeName = typeName;
62
+ }
63
+ }
64
+
43
65
  const PRIMITIVES: Readonly<Record<string, CelType>> = {
44
66
  dyn: DYN,
45
67
  int: INT,
@@ -150,7 +172,7 @@ class TypeReader {
150
172
  if (PARAMETER.test(name)) return this.noArguments(name, args, parameterOf(name));
151
173
  const nominal = this.resolveNominal?.(name, args);
152
174
  if (nominal) return nominal;
153
- throw new CelTypeExpressionError(`no type is registered under the name ${JSON.stringify(name)}`);
175
+ throw new CelUnknownTypeNameError(name);
154
176
  }
155
177
 
156
178
  private noArguments(name: string, args: readonly CelType[], type: CelType): CelType {
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { durationNanos } from "./duration-value.js";
32
32
  import { mapKeyIdentity } from "./cel-map-value.js";
33
- import type { CelValue } from "./cel-value.js";
33
+ import type { CelMapKey, CelValue } from "./cel-value.js";
34
34
  import {
35
35
  celTypeNameOf,
36
36
  isCelBytes,
@@ -171,12 +171,12 @@ function equalLists(left: readonly CelValue[], right: readonly CelValue[]): bool
171
171
  }
172
172
 
173
173
  /** Every key of a map, as the pairs equality walks. */
174
- function mapPairs(value: CelValue): Map<string, CelValue> | undefined {
174
+ function mapPairs(value: CelValue): Map<CelMapKey, CelValue> | undefined {
175
175
  if (isCelMap(value)) {
176
176
  return new Map([...value.entries].map(([identity, entry]) => [identity, entry.value]));
177
177
  }
178
178
  if (isCelRecord(value)) {
179
- const pairs = new Map<string, CelValue>();
179
+ const pairs = new Map<CelMapKey, CelValue>();
180
180
  for (const key of Object.keys(value)) pairs.set(mapKeyIdentity(key)!, value[key] as CelValue);
181
181
  return pairs;
182
182
  }