@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.
- package/dist/backend-runtime.d.ts +46 -8
- package/dist/backend-runtime.d.ts.map +1 -1
- package/dist/backend-runtime.js +133 -30
- package/dist/catalog-runtime.d.ts.map +1 -1
- package/dist/catalog-runtime.js +110 -109
- package/dist/cel-map-value.d.ts +20 -7
- package/dist/cel-map-value.d.ts.map +1 -1
- package/dist/cel-map-value.js +29 -18
- package/dist/cel-value.d.ts +10 -3
- package/dist/cel-value.d.ts.map +1 -1
- package/dist/check-diagnostic.d.ts +3 -2
- package/dist/check-diagnostic.d.ts.map +1 -1
- package/dist/checker.d.ts +9 -0
- package/dist/checker.d.ts.map +1 -1
- package/dist/checker.js +98 -13
- package/dist/closure-backend.js +66 -15
- package/dist/duration-value.d.ts +11 -0
- package/dist/duration-value.d.ts.map +1 -1
- package/dist/duration-value.js +17 -3
- package/dist/emitted-module.d.ts +1 -1
- package/dist/emitted-module.js +1 -1
- package/dist/engine-version.d.ts +1 -1
- package/dist/engine-version.js +1 -1
- package/dist/environment.d.ts +8 -0
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +20 -5
- package/dist/function-registry.d.ts +40 -0
- package/dist/function-registry.d.ts.map +1 -1
- package/dist/function-registry.js +105 -2
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -6
- package/dist/js-emitter.d.ts +3 -2
- package/dist/js-emitter.d.ts.map +1 -1
- package/dist/js-emitter.js +11 -5
- package/dist/macro-check.d.ts +8 -0
- package/dist/macro-check.d.ts.map +1 -1
- package/dist/macro-check.js +1 -1
- package/dist/member-read.d.ts +42 -5
- package/dist/member-read.d.ts.map +1 -1
- package/dist/member-read.js +47 -6
- package/dist/resolved-call.d.ts +13 -0
- package/dist/resolved-call.d.ts.map +1 -1
- package/dist/runtime-library.d.ts +13 -2
- package/dist/runtime-library.d.ts.map +1 -1
- package/dist/runtime-library.js +134 -125
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +20 -4
- package/dist/timestamp-value.d.ts +9 -0
- package/dist/timestamp-value.d.ts.map +1 -1
- package/dist/timestamp-value.js +15 -0
- package/dist/type-expression.d.ts +18 -2
- package/dist/type-expression.d.ts.map +1 -1
- package/dist/type-expression.js +23 -3
- package/dist/value-equality.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/backend-runtime.ts +180 -30
- package/src/catalog-runtime.ts +133 -133
- package/src/cel-map-value.ts +31 -20
- package/src/cel-value.ts +11 -3
- package/src/check-diagnostic.ts +4 -2
- package/src/checker.ts +123 -10
- package/src/closure-backend.ts +59 -14
- package/src/duration-value.ts +18 -3
- package/src/emitted-module.ts +1 -1
- package/src/engine-version.ts +1 -1
- package/src/environment.ts +26 -5
- package/src/function-registry.ts +114 -2
- package/src/index.ts +24 -3
- package/src/js-emitter.ts +13 -5
- package/src/macro-check.ts +7 -1
- package/src/member-read.ts +48 -7
- package/src/resolved-call.ts +13 -0
- package/src/runtime-library.ts +167 -143
- package/src/serializer.ts +19 -3
- package/src/timestamp-value.ts +16 -0
- package/src/type-expression.ts +25 -3
- package/src/value-equality.ts +3 -3
package/src/runtime-library.ts
CHANGED
|
@@ -78,19 +78,42 @@ export interface CelCallContext {
|
|
|
78
78
|
readonly range?: SourceRange;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
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
|
-
|
|
254
|
-
|
|
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 (
|
|
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)", (
|
|
287
|
+
["bool(bool)", (ctx, a) => value(a)],
|
|
264
288
|
[
|
|
265
289
|
"bool(string)",
|
|
266
|
-
(
|
|
267
|
-
const held = BOOL_WORDS[text(
|
|
268
|
-
return held === undefined ? conversionError("bool()", JSON.stringify(
|
|
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)", (
|
|
272
|
-
["bytes(string)", (
|
|
273
|
-
["double(double)", (
|
|
274
|
-
["double(int)", (
|
|
275
|
-
["double(uint)", (
|
|
276
|
-
["double(string)", (
|
|
277
|
-
["duration(string)", (
|
|
278
|
-
["duration(google.protobuf.Duration)", (
|
|
279
|
-
["dyn(A)", (
|
|
280
|
-
["int(int)", (
|
|
281
|
-
["int(uint)", (
|
|
282
|
-
["int(double)", (
|
|
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
|
-
(
|
|
286
|
-
const written = text(
|
|
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)", (
|
|
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)", (
|
|
294
|
-
["matches(string, string)", (
|
|
295
|
-
["size(string)", (
|
|
296
|
-
["size(bytes)", (
|
|
297
|
-
["size(list<A>)", (
|
|
298
|
-
["size(map<K, V>)", (
|
|
299
|
-
["string(string)", (
|
|
300
|
-
["string(bool)", (
|
|
301
|
-
["string(int)", (
|
|
302
|
-
["string(uint)", (
|
|
303
|
-
["string(double)", (
|
|
304
|
-
["string(bytes)", (
|
|
305
|
-
["string(google.protobuf.Timestamp)", (
|
|
306
|
-
["string(google.protobuf.Duration)", (
|
|
307
|
-
["timestamp(string)", (
|
|
308
|
-
["timestamp(int)", (
|
|
309
|
-
["timestamp(google.protobuf.Timestamp)", (
|
|
310
|
-
["uint(uint)", (
|
|
311
|
-
["uint(int)", (
|
|
312
|
-
["uint(double)", (
|
|
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
|
-
(
|
|
316
|
-
const written = text(
|
|
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()", (
|
|
324
|
-
["bytes.size()", (
|
|
325
|
-
["list<A>.size()", (
|
|
326
|
-
["map<K, V>.size()", (
|
|
327
|
-
["string.startsWith(string)", (
|
|
328
|
-
["string.endsWith(string)", (
|
|
329
|
-
["string.contains(string)", (
|
|
330
|
-
["string.matches(string)", (
|
|
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()", (
|
|
333
|
-
["string.upperAscii()", (
|
|
334
|
-
["string.trim()", (
|
|
335
|
-
["string.indexOf(string)", (
|
|
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
|
-
(
|
|
339
|
-
const from = boundedIndex(integer(
|
|
340
|
-
return typeof from === "number" ? BigInt(text(
|
|
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)", (
|
|
367
|
+
["string.lastIndexOf(string)", (ctx, a, b) => BigInt(text(a).lastIndexOf(text(b)))],
|
|
344
368
|
[
|
|
345
369
|
"string.lastIndexOf(string, int)",
|
|
346
|
-
(
|
|
347
|
-
const from = boundedIndex(integer(
|
|
348
|
-
return typeof from === "number" ? BigInt(text(
|
|
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)", (
|
|
375
|
+
["string.substring(int)", (ctx, a, b) => substring(text(a), integer(b), undefined, ctx.range)],
|
|
352
376
|
[
|
|
353
377
|
"string.substring(int, int)",
|
|
354
|
-
(
|
|
378
|
+
(ctx, a, b, c) => substring(text(a), integer(b), integer(c), ctx.range),
|
|
355
379
|
],
|
|
356
|
-
["string.split(string)", (
|
|
357
|
-
["string.split(string, int)", (
|
|
358
|
-
["list<string>.join()", (
|
|
359
|
-
["list<string>.join(string)", (
|
|
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()", (
|
|
363
|
-
["bytes.hex()", (
|
|
364
|
-
["bytes.base64()", (
|
|
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
|
-
(
|
|
368
|
-
const decoded = bytesToText(bytes(
|
|
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
|
-
(
|
|
380
|
-
const held = bytes(
|
|
381
|
-
const at = integer(
|
|
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)", (
|
|
391
|
-
["-(int)", (
|
|
392
|
-
["-(double)", (
|
|
393
|
-
["+(int, int)", (
|
|
394
|
-
["+(uint, uint)", (
|
|
395
|
-
["+(double, double)", (
|
|
396
|
-
["+(string, string)", (
|
|
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
|
-
(
|
|
400
|
-
const left = bytes(
|
|
401
|
-
const right = bytes(
|
|
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>)", (
|
|
432
|
+
["+(list<A>, list<A>)", (ctx, a, b) => [...list(a), ...list(b)]],
|
|
409
433
|
[
|
|
410
434
|
"+(google.protobuf.Timestamp, google.protobuf.Duration)",
|
|
411
|
-
(
|
|
412
|
-
durationOutOfRange(span(
|
|
413
|
-
celTimestamp(0n, timestampNanos(instant(
|
|
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
|
-
(
|
|
418
|
-
durationOutOfRange(span(
|
|
419
|
-
celTimestamp(0n, durationNanos(span(
|
|
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
|
-
(
|
|
447
|
+
(ctx, a, b) => celDurationFromNanos(durationNanos(span(a)) + durationNanos(span(b)), ctx.range),
|
|
424
448
|
],
|
|
425
|
-
["-(int, int)", (
|
|
426
|
-
["-(uint, uint)", (
|
|
427
|
-
["-(double, double)", (
|
|
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
|
-
(
|
|
431
|
-
celDurationFromNanos(timestampNanos(instant(
|
|
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
|
-
(
|
|
436
|
-
durationOutOfRange(span(
|
|
437
|
-
celTimestamp(0n, timestampNanos(instant(
|
|
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
|
-
(
|
|
465
|
+
(ctx, a, b) => celDurationFromNanos(durationNanos(span(a)) - durationNanos(span(b)), ctx.range),
|
|
442
466
|
],
|
|
443
|
-
["*(int, int)", (
|
|
444
|
-
["*(uint, uint)", (
|
|
445
|
-
["*(double, double)", (
|
|
446
|
-
["/(int, int)", (
|
|
447
|
-
["/(uint, uint)", (
|
|
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)", (
|
|
450
|
-
["%(int, int)", (
|
|
451
|
-
["%(uint, uint)", (
|
|
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
|
-
(
|
|
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(
|
|
482
|
+
for (const held of list(b)) {
|
|
459
483
|
const refused = asyncValueRefused(held);
|
|
460
484
|
if (refused) return refused;
|
|
461
|
-
if (celEqual(held,
|
|
485
|
+
if (celEqual(held, value(a))) return true;
|
|
462
486
|
}
|
|
463
487
|
return false;
|
|
464
488
|
},
|
|
465
489
|
],
|
|
466
|
-
["in(K, map<K, V>)", (
|
|
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()", (
|
|
493
|
+
["optional<A>.hasValue()", (ctx, a) => optional(a).present],
|
|
470
494
|
[
|
|
471
495
|
"optional<A>.value()",
|
|
472
|
-
(
|
|
473
|
-
const held = optional(
|
|
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>)", (
|
|
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
|
-
(
|
|
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
|
-
["==", (
|
|
526
|
-
["!=", (
|
|
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", (
|
|
555
|
+
["type", (ctx, a) => celTypeValueOf(value(a), ctx.range)],
|
|
532
556
|
]);
|
|
533
557
|
|
|
534
558
|
function ordering(holds: (compared: number) => boolean): CelImplementation {
|
|
535
|
-
return (
|
|
536
|
-
const compared = celCompare(
|
|
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(
|
|
564
|
+
if (isNotANumber(a) || isNotANumber(b)) return false;
|
|
541
565
|
return celError(
|
|
542
566
|
"no_matching_overload",
|
|
543
|
-
`${celTypeNameOf(
|
|
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(
|
|
594
|
-
return isZeroValue(
|
|
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
|
-
|
|
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 ===
|
|
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 {
|
package/src/timestamp-value.ts
CHANGED
|
@@ -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);
|
package/src/type-expression.ts
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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
|
|
175
|
+
throw new CelUnknownTypeNameError(name);
|
|
154
176
|
}
|
|
155
177
|
|
|
156
178
|
private noArguments(name: string, args: readonly CelType[], type: CelType): CelType {
|
package/src/value-equality.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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
|
}
|