@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.
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
@@ -39,6 +39,7 @@ import {
39
39
  import type { CelError, CelOptional, CelValue } from "./cel-value.js";
40
40
  import {
41
41
  asyncValueRefused,
42
+ CEL_VALUE_TYPE,
42
43
  celError,
43
44
  celNone,
44
45
  celSome,
@@ -47,7 +48,7 @@ import {
47
48
  isCelOptional,
48
49
  } from "./cel-value.js";
49
50
  import type { FunctionRegistry, Resolution, ResolutionFailure } from "./function-registry.js";
50
- import { celHas, celLookup, celRead, lookupError, MISSING, OUT_OF_RANGE } from "./member-read.js";
51
+ import { celHas, celLookup, celRead, lookupAbsence, lookupError } from "./member-read.js";
51
52
  import type { CelCallContext, CelImplementation } from "./runtime-library.js";
52
53
  import { implementationOf } from "./runtime-library.js";
53
54
  import type { CallForm } from "./signature.js";
@@ -65,7 +66,19 @@ export class CelCompileError extends Error {
65
66
  export const CALL_SITE_CACHE_CAPACITY = 16;
66
67
 
67
68
  /** What a namespaced call is dispatched through, when something bound it. */
68
- export type NamespaceDispatch = (namespace: string, name: string) => CelImplementation | undefined;
69
+ /**
70
+ * A function bound under a namespace. Its arguments arrive as **one array**, where a
71
+ * registered overload's arrive positionally: a namespace declaration may withhold its
72
+ * parameter list entirely, so the count is the host's and not the dispatch key's. That is
73
+ * the whole rule — positional where the key fixes the arity, one array where the author
74
+ * decides it.
75
+ */
76
+ export type NamespaceImplementation = (args: readonly CelValue[], ctx: CelCallContext) => CelValue;
77
+
78
+ export type NamespaceDispatch = (
79
+ namespace: string,
80
+ name: string,
81
+ ) => NamespaceImplementation | undefined;
69
82
 
70
83
  export interface EvaluationFrame {
71
84
  readonly activation: CelActivation;
@@ -211,9 +224,13 @@ export function plainMemberChain(
211
224
  /**
212
225
  * A member read in every form. Reading **through an optional** answers an optional
213
226
  * whichever form the read is written in, which is what lets a chain over a value that
214
- * may be absent stay one expression: an absent one propagates as absent, a key the held
215
- * value does not have is absent too, and a held value that holds no members at all is
216
- * still the mistake it would be outside an optional.
227
+ * may be absent stay one expression: an absent one propagates as absent, and a key the
228
+ * held value does not have is absent too.
229
+ *
230
+ * Which refusals are absence and which are a mistake is `lookupAbsence`'s, and it turns
231
+ * on the FORM the read is written in rather than on what the operand turned out to be: a
232
+ * presence-shaped read over a value that holds no members is absent, while the ordinary
233
+ * read of a member of such a value is the mistake it is outside an optional too.
217
234
  */
218
235
  export function readThrough(
219
236
  container: CelValue,
@@ -221,31 +238,61 @@ export function readThrough(
221
238
  optionalForm: boolean,
222
239
  range: SourceRange,
223
240
  ): CelValue {
241
+ // The shape nearly every read has — a record's own string-keyed entry — decided by asking
242
+ // what the container is ONCE. The general path below is three total steps, so it reads the
243
+ // type key twice and the prototype again; this asks each question once and falls through
244
+ // to the one seam for every other container, every other key and every refusal.
245
+ if (!optionalForm && typeof key === "string" && typeof container === "object" && container !== null) {
246
+ if ((container as { [CEL_VALUE_TYPE]?: unknown })[CEL_VALUE_TYPE] === undefined) {
247
+ const prototype = Object.getPrototypeOf(container) as object | null;
248
+ if (
249
+ (prototype === Object.prototype || prototype === null) &&
250
+ Object.prototype.hasOwnProperty.call(container, key)
251
+ ) {
252
+ const held = (container as Record<string, CelValue>)[key] as CelValue;
253
+ // The thenable door, inline: `readHostValue` is this test and nothing else.
254
+ if (typeof held !== "object" || held === null || typeof (held as { then?: unknown }).then !== "function") {
255
+ return held;
256
+ }
257
+ }
258
+ }
259
+ }
224
260
  if (isCelOptional(container)) {
225
261
  if (!container.present) return celNone();
226
- return optionalRead(container.held as CelValue, key, range);
262
+ return optionalRead(container.held as CelValue, key, optionalForm, range);
227
263
  }
228
- if (optionalForm) return optionalRead(container, key, range);
264
+ if (optionalForm) return optionalRead(container, key, true, range);
229
265
  // **A member read is a door a host value comes through**, and the value it answers is
230
266
  // whatever the host put inside its own object: `readHostValue` is what refuses a thenable
231
267
  // there, rather than the aggregate, the operator or the exit that happens to see it next.
232
268
  return readHostValue(celRead(container, key, range), range);
233
269
  }
234
270
 
235
- function optionalRead(container: CelValue, key: CelValue, range: SourceRange): CelValue {
271
+ /**
272
+ * A read that answers an optional. `presence` is whether the read was WRITTEN in a
273
+ * presence-shaped form (`.?`, `[?]`), which is what decides whether a value holding no
274
+ * members is absence or a mistake.
275
+ */
276
+ function optionalRead(
277
+ container: CelValue,
278
+ key: CelValue,
279
+ presence: boolean,
280
+ range: SourceRange,
281
+ ): CelValue {
236
282
  const found = celLookup(container, key);
237
283
  if (typeof found !== "symbol") {
238
284
  const held = readHostValue(found, range);
239
285
  // A value that must be awaited is refused rather than carried as a present optional.
240
286
  return isCelError(held) ? held : celSome(held);
241
287
  }
242
- if (found === MISSING || found === OUT_OF_RANGE) return celNone();
288
+ if (lookupAbsence(found, presence)) return celNone();
243
289
  return lookupError(found, key, range);
244
290
  }
245
291
 
246
292
  /**
247
- * `has(a.b)` — presence, which a missing key answers `false` for rather than erroring.
248
- * An absent optional has no members, and a present one is asked about what it holds.
293
+ * `has(a.b)` — presence, which a missing key and a value that holds no members both
294
+ * answer `false` for rather than erroring. An absent optional has no members, and a
295
+ * present one is asked about what it holds.
249
296
  */
250
297
  export function hasMember(container: CelValue, field: CelValue, range: SourceRange): CelValue {
251
298
  if (isCelOptional(container)) {
@@ -318,7 +365,12 @@ function equalityFallback(name: string): CelImplementation | null {
318
365
  export class CallSite {
319
366
  private readonly resolved = new BoundedCache<string, Dispatch | null>(CALL_SITE_CACHE_CAPACITY);
320
367
  private readonly context: CelCallContext;
321
- private lastNames: readonly string[] | undefined;
368
+ /** The arity the last resolution was made under; `-1` until there is one. */
369
+ private count = -1;
370
+ private t0: string | undefined;
371
+ private t1: string | undefined;
372
+ private t2: string | undefined;
373
+ private t3: string | undefined;
322
374
  private lastDispatch: Dispatch | null = null;
323
375
 
324
376
  constructor(
@@ -331,22 +383,111 @@ export class CallSite {
331
383
  this.context = { range };
332
384
  }
333
385
 
386
+ call0(): CelValue {
387
+ if (this.count === 0) return this.answer(this.lastDispatch);
388
+ return this.resolve(0, undefined, undefined, undefined, undefined);
389
+ }
390
+
391
+ call1(a: CelValue): CelValue {
392
+ if (this.count === 1 && celTypeNameOf(a) === this.t0) return this.answer(this.lastDispatch, a);
393
+ return this.resolve(1, a, undefined, undefined, undefined);
394
+ }
395
+
396
+ call2(a: CelValue, b: CelValue): CelValue {
397
+ if (this.count === 2 && celTypeNameOf(a) === this.t0 && celTypeNameOf(b) === this.t1) {
398
+ return this.answer(this.lastDispatch, a, b);
399
+ }
400
+ return this.resolve(2, a, b, undefined, undefined);
401
+ }
402
+
403
+ call3(a: CelValue, b: CelValue, c: CelValue): CelValue {
404
+ if (
405
+ this.count === 3 &&
406
+ celTypeNameOf(a) === this.t0 &&
407
+ celTypeNameOf(b) === this.t1 &&
408
+ celTypeNameOf(c) === this.t2
409
+ ) {
410
+ return this.answer(this.lastDispatch, a, b, c);
411
+ }
412
+ return this.resolve(3, a, b, c, undefined);
413
+ }
414
+
415
+ call4(a: CelValue, b: CelValue, c: CelValue, d: CelValue): CelValue {
416
+ if (
417
+ this.count === 4 &&
418
+ celTypeNameOf(a) === this.t0 &&
419
+ celTypeNameOf(b) === this.t1 &&
420
+ celTypeNameOf(c) === this.t2 &&
421
+ celTypeNameOf(d) === this.t3
422
+ ) {
423
+ return this.answer(this.lastDispatch, a, b, c, d);
424
+ }
425
+ return this.resolve(4, a, b, c, d);
426
+ }
427
+
428
+ /**
429
+ * The array form, for a caller that holds its arguments as one — a call written WIDER than
430
+ * any overload can be (`'42'.replace('2', '1', 1, false)` is five values, which is a row),
431
+ * the conformance and identity gates, and a host comparing a site cold against warm. It
432
+ * routes to the same entry points, so there is one dispatch path and not two.
433
+ */
334
434
  call(values: readonly CelValue[]): CelValue {
335
- const count = values.length;
336
- const last = this.lastNames;
337
- if (last !== undefined && last.length === count) {
338
- let same = true;
339
- for (let at = 0; at < count; at += 1) {
340
- if (celTypeNameOf(values[at]!) !== last[at]) {
341
- same = false;
342
- break;
343
- }
435
+ switch (values.length) {
436
+ case 0:
437
+ return this.call0();
438
+ case 1:
439
+ return this.call1(values[0]!);
440
+ case 2:
441
+ return this.call2(values[0]!, values[1]!);
442
+ case 3:
443
+ return this.call3(values[0]!, values[1]!, values[2]!);
444
+ case 4:
445
+ return this.call4(values[0]!, values[1]!, values[2]!, values[3]!);
446
+ default:
447
+ return this.wide(values);
448
+ }
449
+ }
450
+
451
+ /**
452
+ * A call written with more values than the widest signature may declare. **The arity is
453
+ * the SOURCE's, not the dispatch key's** — anyone may write a call of any width — so this
454
+ * is reachable and answers the ordinary refusal, naming the types it was handed. Nothing
455
+ * resolves here, because a signature that wide is refused where it is registered; the
456
+ * resolution still runs, so one place decides what a call that resolves to nothing says.
457
+ */
458
+ private wide(values: readonly CelValue[]): CelValue {
459
+ const names: string[] = new Array<string>(values.length);
460
+ for (let at = 0; at < values.length; at += 1) {
461
+ const held = celTypeNameOf(values[at]);
462
+ if (held === undefined) {
463
+ const named = readHostValue(values[at], this.range);
464
+ if (isCelError(named)) return named;
465
+ return celError(
466
+ "no_matching_overload",
467
+ `${this.name} was handed a value of no CEL type`,
468
+ this.range,
469
+ );
344
470
  }
345
- if (same) return this.answer(this.lastDispatch, values, last);
471
+ names[at] = held;
346
472
  }
473
+ return celError(
474
+ "no_matching_overload",
475
+ `no overload of ${JSON.stringify(this.name)} takes (${names.join(", ")})`,
476
+ this.range,
477
+ );
478
+ }
479
+
480
+ private resolve(
481
+ count: number,
482
+ a: CelValue | undefined,
483
+ b: CelValue | undefined,
484
+ c: CelValue | undefined,
485
+ d: CelValue | undefined,
486
+ ): CelValue {
347
487
  const names: string[] = new Array<string>(count);
348
488
  for (let at = 0; at < count; at += 1) {
349
- const held = celTypeNameOf(values[at]!);
489
+ const value = at === 0 ? a : at === 1 ? b : at === 2 ? c : d;
490
+ const held = celTypeNameOf(value);
350
491
  if (held === undefined) {
351
492
  // A value of no CEL type. A **thenable** is one, and a thenable NESTED inside a host
352
493
  // value arrives here rather than through the activation read, because a member read
@@ -354,7 +495,7 @@ export class CallSite {
354
495
  // shape, so this is the door a host actually uses. `readHostValue` names it for what
355
496
  // it is; anything else is the overload failure it was going to be. The cost is on the
356
497
  // slow path only: dispatch was about to fail either way.
357
- const named = readHostValue(values[at], this.range);
498
+ const named = readHostValue(value, this.range);
358
499
  if (isCelError(named)) return named;
359
500
  return celError(
360
501
  "no_matching_overload",
@@ -367,7 +508,9 @@ export class CallSite {
367
508
  const key = names.join(",");
368
509
  let dispatch = this.resolved.get(key);
369
510
  if (dispatch === undefined) {
370
- const types = values.map((value) => runtimeType(value, this.nominalArity));
511
+ const types = names.map((unused, at) =>
512
+ runtimeType((at === 0 ? a : at === 1 ? b : at === 2 ? c : d) as CelValue, this.nominalArity),
513
+ );
371
514
  const resolution = this.registry.resolve(
372
515
  this.name,
373
516
  this.form,
@@ -377,24 +520,31 @@ export class CallSite {
377
520
  dispatch = dispatchOf(this.name, resolution);
378
521
  this.resolved.set(key, dispatch);
379
522
  }
380
- this.lastNames = names;
523
+ this.count = count;
524
+ this.t0 = names[0];
525
+ this.t1 = names[1];
526
+ this.t2 = names[2];
527
+ this.t3 = names[3];
381
528
  this.lastDispatch = dispatch;
382
- return this.answer(dispatch, values, names);
529
+ return this.answer(dispatch, a, b, c, d);
383
530
  }
384
531
 
385
532
  private answer(
386
533
  dispatch: Dispatch | null,
387
- values: readonly CelValue[],
388
- names: readonly string[],
534
+ a?: CelValue,
535
+ b?: CelValue,
536
+ c?: CelValue,
537
+ d?: CelValue,
389
538
  ): CelValue {
390
539
  if (!dispatch) {
540
+ const names = [this.t0, this.t1, this.t2, this.t3].slice(0, Math.max(this.count, 0));
391
541
  return celError(
392
542
  "no_matching_overload",
393
543
  `no overload of ${JSON.stringify(this.name)} takes (${names.join(", ")})`,
394
544
  this.range,
395
545
  );
396
546
  }
397
- const value = dispatch.implementation(values, this.context);
547
+ const value = dispatch.implementation(this.context, a, b, c, d);
398
548
  return dispatch.foreign ? readHostValue(value, this.range) : value;
399
549
  }
400
550
  }