@libdbm/libcel-ts 1.0.2-rc.1 → 2.0.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/README.md CHANGED
@@ -9,11 +9,14 @@ CEL is a non-Turing complete expression language designed for simplicity, speed,
9
9
  ## Features
10
10
 
11
11
  - **Complete CEL Implementation**: All CEL operators, functions, and macros
12
+ - **Extension Libraries**: strings, lists, sets, math, regex and base64 functions using the cel-go names
13
+ - **Round-trip Printing**: render a parsed expression back to CEL source, compact or pretty printed
14
+ - **Exact 64-bit Integers**: CEL ints are `bigint`, with overflow detection and exact mixed int/double comparison
12
15
  - **Type Safe**: Leverages TypeScript's type system with strict typing
13
16
  - **High Performance**: Hand-written recursive descent parser with AST compilation
14
17
  - **Extensible**: Easy to add custom functions
15
- - **Well Tested**: 100+ comprehensive tests ensuring functional equivalence
16
- - **Zero External Dependencies**: Pure TypeScript implementation (except dev dependencies)
18
+ - **Well Tested**: 275 comprehensive tests, including a specification conformance suite
19
+ - **Zero External Dependencies**: Pure TypeScript implementation, runs in Node and browsers
17
20
  - **Vite-Powered**: Modern tooling with fast builds and excellent DX
18
21
 
19
22
  ## Installation
@@ -35,8 +38,8 @@ import { CEL } from '@libdbm/libcel-ts';
35
38
 
36
39
  const cel = new CEL();
37
40
 
38
- // Simple expression evaluation
39
- console.log(cel.eval('2 + 3 * 4', {})); // 14
41
+ // Simple expression evaluation; CEL ints are JavaScript bigints
42
+ console.log(cel.eval('2 + 3 * 4', {})); // 14n
40
43
 
41
44
  // Using variables
42
45
  const vars = { name: 'Alice', age: 30 };
@@ -56,8 +59,8 @@ const cel = new CEL();
56
59
  const program = cel.compile('price * quantity * (1 - discount)');
57
60
 
58
61
  // Reuse with different variables
59
- const result1 = program.evaluate({ price: 10, quantity: 5, discount: 0.1 });
60
- const result2 = program.evaluate({ price: 20, quantity: 3, discount: 0.2 });
62
+ const result1 = program.evaluate({ price: 10, quantity: 5, discount: 0.1 }); // 45
63
+ const result2 = program.evaluate({ price: 20, quantity: 3, discount: 0.2 }); // 48
61
64
  ```
62
65
 
63
66
  ### Working with Complex Data
@@ -96,9 +99,11 @@ Extend the standard library with custom functions:
96
99
  import { CEL, StandardFunctions } from '@libdbm/libcel-ts';
97
100
 
98
101
  class CustomFunctions extends StandardFunctions {
99
- callFunction(name: string, args: any[]): any {
102
+ override callFunction(name: string, args: unknown[]): unknown {
100
103
  if (name === 'reverse') {
101
- return (args[0] as string).split('').reverse().join('');
104
+ return Array.from(args[0] as string)
105
+ .reverse()
106
+ .join('');
102
107
  }
103
108
  return super.callFunction(name, args);
104
109
  }
@@ -108,6 +113,37 @@ const cel = new CEL(new CustomFunctions());
108
113
  console.log(cel.eval("reverse('hello')", {})); // "olleh"
109
114
  ```
110
115
 
116
+ Results returned by custom functions are converted into the CEL value model: plain objects
117
+ become maps and `Date` becomes a timestamp. A JavaScript `number` returned by a function is a
118
+ CEL double; return a `bigint` to produce a CEL int.
119
+
120
+ ## Value Model
121
+
122
+ CEL values are represented by JavaScript values as follows.
123
+
124
+ | CEL type | Result type | Accepted as a variable |
125
+ | ----------- | ------------------------------ | ------------------------------------------------------ |
126
+ | `int` | `bigint` | `bigint`, or a `number` with an integer value |
127
+ | `uint` | `bigint` (not a distinct type) | as `int` |
128
+ | `double` | `number` | a `number` with a fractional part, `NaN` or infinities |
129
+ | `bool` | `boolean` | `boolean` |
130
+ | `string` | `string` | `string` |
131
+ | `bytes` | `Uint8Array` | `Uint8Array` |
132
+ | `list` | `Array` | `Array` |
133
+ | `map` | `Map` | `Map`, or a plain object (string keys) |
134
+ | `null` | `null` | `null` or `undefined` |
135
+ | `timestamp` | `Timestamp` | `Timestamp` or `Date` |
136
+ | `duration` | `Duration` | `Duration` |
137
+ | `type` | `Type` | `Type` |
138
+
139
+ Variables are normalised when evaluation starts: an integer-valued `number` such as `30` is a
140
+ CEL int, so `age / 7` truncates. Write `double(age)` in the expression when a double is wanted,
141
+ or pass a value with a fractional part. Map literals evaluate to `Map`, and maps may use keys
142
+ of any type; a key stored as `1` matches a probe of `1.0`.
143
+
144
+ `bigint` results cannot be passed to `JSON.stringify` directly; supply a replacer such as
145
+ `(_, v) => (typeof v === 'bigint' ? Number(v) : v)`.
146
+
111
147
  ## Supported Features
112
148
 
113
149
  ### Literals
@@ -118,35 +154,142 @@ console.log(cel.eval("reverse('hello')", {})); // "olleh"
118
154
  - Unsigned: `42u`, `0xFFu`
119
155
  - Doubles: `3.14`, `6.022e23`
120
156
  - Strings: `"hello"`, `'world'`, `r"raw\nstring"`, `"""multi-line"""`
121
- - Bytes: `b"data"`
157
+ - Bytes: `b"data"`, `b"\x00\xff"`
122
158
  - Lists: `[1, 2, 3]`
123
159
  - Maps: `{"key": "value"}`
124
160
 
125
161
  ### Operators
126
162
 
127
163
  - **Arithmetic**: `+`, `-`, `*`, `/`, `%`
128
- - **Comparison**: `<`, `<=`, `>`, `>=`, `==`, `!=`
164
+ - **Comparison**: `<`, `<=`, `>`, `>=`, `==`, `!=`, compared exactly across int and double, so an
165
+ int beyond the range a double can represent is not mistaken for the double it would round to
129
166
  - **Logical**: `&&`, `||`, `!`
130
167
  - **Conditional**: `condition ? trueValue : falseValue`
131
168
  - **Membership**: `in` (for lists, maps, strings)
132
169
 
133
170
  ### Functions
134
171
 
135
- - **Type conversions**: `int()`, `double()`, `string()`, `bool()`
136
- - **Type checking**: `type()`
172
+ - **Type conversions**: `int()`, `uint()`, `double()`, `string()`, `bool()`, `bytes()`, `dyn()`
173
+ - **Type checking**: `type()`, which returns a type value comparable against the bare type names
174
+ `null_type`, `bool`, `int`, `uint`, `double`, `string`, `bytes`, `list`, `map`, `timestamp`,
175
+ `duration` and `type`
137
176
  - **Collections**: `size()`, `has()`
138
- - **String methods**: `contains()`, `startsWith()`, `endsWith()`, `toLowerCase()`, `toUpperCase()`, `trim()`, `replace()`, `split()`
139
- - **Regex**: `matches()`
140
177
  - **Math**: `max()`, `min()`
178
+ - **Regex**: `matches()`, as a function and as a string method
179
+
180
+ ### String functions
181
+
182
+ Receiver style, following the cel-go strings extension:
183
+
184
+ `charAt()`, `indexOf()`, `lastIndexOf()`, `lowerAscii()`, `upperAscii()`, `substring()`, `reverse()`,
185
+ `trim()`, `contains()`, `startsWith()`, `endsWith()`, `replace(from, to[, limit])`,
186
+ `split(separator[, limit])`, `join([separator])` on a list of strings, `format(list)`, and the global
187
+ `strings.quote()`.
188
+
189
+ ```typescript
190
+ cel.eval("'hello'.charAt(1)", {}); // "e"
191
+ cel.eval("'a,b,c'.split(',', 2)", {}); // ["a", "b,c"]
192
+ cel.eval("['a','b'].join('-')", {}); // "a-b"
193
+ cel.eval("'%d apples at %.2f'.format([3, 1.5])", {}); // "3 apples at 1.50"
194
+ ```
195
+
196
+ `format()` supports the verbs `%s %d %f %e %b %o %x %X %%` with an optional precision such as `%.3f`.
197
+
198
+ Positions and sizes count Unicode code points, not UTF-16 code units, so `charAt()`, `substring()`,
199
+ `indexOf()`, `lastIndexOf()`, string indexing and `size()` never split a supplementary character:
200
+
201
+ ```typescript
202
+ cel.eval("size('\\U0001F600b')", {}); // 2n
203
+ cel.eval("'\\U0001F600b'.charAt(0)", {}); // the whole emoji, not a lone surrogate
204
+ ```
205
+
206
+ ### Regex functions
207
+
208
+ ```typescript
209
+ cel.eval("regex.replace('a b c', ' ', '-')", {}); // "a-b-c"
210
+ cel.eval("regex.replace('ab', '(\\\\w)', '[\\\\1]')", {}); // "[a][b]"
211
+ cel.eval("regex.extract('id=123', '=([0-9]+)')", {}); // "123"
212
+ cel.eval("regex.extractAll('a1b2', '[0-9]')", {}); // ["1", "2"]
213
+ ```
214
+
215
+ `regex.replace()` takes an optional fourth argument limiting the number of replacements. Capture
216
+ groups are referenced as `\1` through `\9`, and `\0` for the whole match. Patterns use the
217
+ JavaScript `RegExp` dialect.
218
+
219
+ ### List and set functions
220
+
221
+ ```typescript
222
+ cel.eval('[1, 2, 2, 3].distinct()', {}); // [1n, 2n, 3n]
223
+ cel.eval('[[1, 2], [3]].flatten()', {}); // [1n, 2n, 3n]
224
+ cel.eval('[3, 1, 2].sort()', {}); // [1n, 2n, 3n]
225
+ cel.eval('[1, 2, 3, 4].slice(1, 3)', {}); // [2n, 3n]
226
+ cel.eval('[1, 2, 3].reverse()', {}); // [3n, 2n, 1n]
227
+ cel.eval('[1, 2].first()', {}); // 1n
228
+ cel.eval('lists.range(3)', {}); // [0n, 1n, 2n]
229
+ cel.eval('sets.contains([1, 2, 3], [1, 3])', {}); // true
230
+ cel.eval('sets.equivalent([1, 2, 2], [2, 1])', {}); // true
231
+ cel.eval('sets.intersects([1, 2], [2, 3])', {}); // true
232
+ ```
233
+
234
+ ### Math functions
235
+
236
+ `math.greatest()`, `math.least()`, `math.abs()`, `math.ceil()`, `math.floor()`, `math.round()`,
237
+ `math.trunc()`, `math.sign()`, `math.sqrt()`, `math.isNaN()`, `math.isInf()`, `math.isFinite()`,
238
+ `math.bitAnd()`, `math.bitOr()`, `math.bitXor()`, `math.bitNot()`, `math.bitShiftLeft()`,
239
+ `math.bitShiftRight()`.
240
+
241
+ `abs()` and `sign()` preserve the numeric type of their argument; `math.greatest()` and
242
+ `math.least()` also accept a single list. Bit operations work on the full signed 64-bit range.
243
+
244
+ ### Bytes and base64
245
+
246
+ Bytes literals evaluate to a `Uint8Array`. They support `size()`, indexing, concatenation,
247
+ comparison and `string()` conversion (decoded as UTF-8).
248
+
249
+ ```typescript
250
+ cel.eval("base64.encode(b'hello')", {}); // "aGVsbG8="
251
+ cel.eval("string(base64.decode('aGVsbG8='))", {}); // "hello"
252
+ ```
253
+
254
+ ### Timestamps and durations
255
+
256
+ `timestamp()` produces a `Timestamp` and `duration()` a `Duration`; both are exported classes with
257
+ nanosecond precision (`seconds: bigint`, `nanos: number`). A JavaScript `Date` passed as a variable
258
+ is accepted as a timestamp. Durations accept the full CEL syntax: signed, multi-unit and
259
+ fractional values with the units `ns`, `us`, `ms`, `s`, `m` and `h`, for example `1h30m`, `-1.5s`
260
+ or `100ms`. Components are accumulated exactly, and a duration beyond the specification's range
261
+ of `Utilities.SPAN` seconds either side of zero is rejected rather than silently clamped.
262
+
263
+ Accessors are available both as global functions and as methods, and default to UTC. Each takes an
264
+ optional IANA time zone as its last argument (resolved through `Intl.DateTimeFormat`).
265
+
266
+ ```typescript
267
+ cel.eval("timestamp('2024-03-05T14:30:45Z').getFullYear()", {}); // 2024n
268
+ cel.eval("getHours(timestamp('2024-03-05T14:30:45Z'), 'America/New_York')", {}); // 9n
269
+ cel.eval("duration('1h30m').getMinutes()", {}); // 90n
270
+ ```
271
+
272
+ The accessors are `getFullYear`, `getMonth` (zero based), `getDate`, `getDayOfWeek` (Sunday is 0),
273
+ `getDayOfYear` (zero based), `getHours`, `getMinutes`, `getSeconds` and `getMilliseconds`.
274
+
275
+ Timestamps and durations support arithmetic and comparison: `ts + dur`, `dur + ts`, `ts - dur`,
276
+ `ts - ts` (yielding a duration), `dur + dur`, `dur - dur` and `-dur`.
277
+
278
+ ### Namespaced functions
279
+
280
+ `math`, `sets`, `lists`, `strings`, `base64` and `regex` are namespaces, not variables. A variable
281
+ of the same name always wins, so binding `math` in the evaluation variables makes `math.greatest`
282
+ resolve as an ordinary field selection again. Custom function libraries expose namespaced
283
+ functions by implementing `knows(name)`.
141
284
 
142
285
  ### Macro Functions
143
286
 
144
287
  ```typescript
145
288
  // map - Transform each element
146
- cel.eval('[1, 2, 3].map(x, x * 2)', {}); // [2, 4, 6]
289
+ cel.eval('[1, 2, 3].map(x, x * 2)', {}); // [2n, 4n, 6n]
147
290
 
148
291
  // filter - Keep elements matching condition
149
- cel.eval('[1, 2, 3, 4].filter(x, x % 2 == 0)', {}); // [2, 4]
292
+ cel.eval('[1, 2, 3, 4].filter(x, x % 2 == 0)', {}); // [2n, 4n]
150
293
 
151
294
  // exists - Check if any element matches
152
295
  cel.eval('[1, 2, 3].exists(x, x > 2)', {}); // true
@@ -156,8 +299,46 @@ cel.eval('[1, 2, 3].all(x, x > 0)', {}); // true
156
299
 
157
300
  // existsOne - Check if exactly one element matches
158
301
  cel.eval('[1, 2, 3].existsOne(x, x == 2)', {}); // true
302
+
303
+ // has - Test field presence without erroring
304
+ cel.eval('has(user.email)', { user: { name: 'Alice' } }); // false
159
305
  ```
160
306
 
307
+ `sortBy` orders a list by a computed key, and `map` accepts an optional predicate between the
308
+ variable and the transform:
309
+
310
+ ```typescript
311
+ cel.eval('users.sortBy(u, u.age).map(u, u.name)', data);
312
+ cel.eval('[1, 2, 3, 4].map(x, x % 2 == 0, x * 2)', {}); // [4n, 8n]
313
+ ```
314
+
315
+ Macros also accept a map as their target, iterating over its keys. Macro predicates must yield a
316
+ boolean.
317
+
318
+ ## Printing expressions
319
+
320
+ `Printer` renders a parsed expression back into CEL source. The compact form is a single line that
321
+ parses back into an equivalent expression, with parentheses only where precedence requires them.
322
+
323
+ ```typescript
324
+ import { Parser, Printer, PrinterOptions } from '@libdbm/libcel-ts';
325
+
326
+ const expr = new Parser('(a + b) * c').parse();
327
+ Printer.print(expr); // "(a + b) * c"
328
+ Printer.print(new Parser('a + (b * c)').parse()); // "a + b * c"
329
+ ```
330
+
331
+ The pretty form breaks lists, maps, structs, argument lists and logical chains once they no longer
332
+ fit the configured width:
333
+
334
+ ```typescript
335
+ Printer.print(expr, PrinterOptions.pretty()); // 2 space indent, 100 column width
336
+ Printer.print(expr, new PrinterOptions(true, 4, 60));
337
+ ```
338
+
339
+ Original spacing and redundant parentheses are not preserved, and comprehension nodes, which the
340
+ parser never produces, are rendered in a diagnostic form that is not valid CEL.
341
+
161
342
  ## Building
162
343
 
163
344
  ```bash
@@ -173,7 +354,7 @@ npm run test:coverage
173
354
  # Build the library
174
355
  npm run build
175
356
 
176
- # Type check
357
+ # Type check (sources and tests)
177
358
  npm run typecheck
178
359
 
179
360
  # Lint and format
@@ -185,9 +366,13 @@ npm run format
185
366
 
186
367
  The project includes comprehensive test coverage:
187
368
 
188
- - 32 parser tests
189
- - 21 interpreter tests
190
- - 50 integration tests
369
+ - 37 parser tests
370
+ - 26 interpreter tests
371
+ - 60 integration tests
372
+ - 15 printer tests
373
+ - 42 extension function tests
374
+ - 69 conformance tests organized after the google/cel-spec corpus
375
+ - 26 value model tests
191
376
  - All tests from the Java implementation ported and passing
192
377
 
193
378
  ```bash
@@ -203,28 +388,129 @@ npm run test:coverage
203
388
 
204
389
  ## Architecture
205
390
 
206
- - **expression.ts**: Abstract Syntax Tree (AST) with visitor pattern
207
- - **parser.ts**: Hand-written recursive descent parser with integrated lexer
208
- - **interpreter.ts**: AST evaluator using Visitor pattern
209
- - **functions.ts**: Extensible function library
391
+ - **ast/expression.ts**: Abstract Syntax Tree (AST) with visitor pattern
392
+ - **parser/lexer.ts, parser/parser.ts**: Hand-written lexer and recursive descent parser
393
+ - **interpreter/interpreter.ts**: AST evaluator using Visitor pattern
394
+ - **printer/printer.ts**: AST to CEL source renderer, also using Visitor pattern
395
+ - **values/**: The CEL value model: `Type`, `Timestamp`, `Duration`, bytes helpers, exact numeric
396
+ ordering, canonical keys for hashing, and boundary normalisation
397
+ - **functions/functions.ts**: Extensible function library interface
398
+ - **functions/standard-functions.ts**: Built-in and extension function dispatch
399
+ - **functions/strings.ts, lists.ts, sets.ts, maths.ts, regexes.ts, codecs.ts**: Extension
400
+ function implementations
401
+ - **functions/utilities.ts**: Conversions, equality, ordering and time helpers
402
+ - **errors.ts**: `EvaluationError` and `ArgumentError`
210
403
  - **cel.ts**: Main API entry point
211
404
  - **program.ts**: Compiled, reusable programs
212
405
 
213
406
  ## Functional Equivalence
214
407
 
215
- This TypeScript implementation is functionally equivalent to the [Java libcel](https://github.com/libdbm/libcel-java) implementation:
408
+ This TypeScript implementation is functionally equivalent to the [Java libcel](https://github.com/libdbm/libcel-java) implementation (version 2.0.0):
216
409
 
217
410
  - Same AST structure and expression types
218
411
  - Identical parsing rules and operator precedence
219
- - Same evaluation semantics
412
+ - Same evaluation semantics, including exact 64-bit integer arithmetic
220
413
  - Equivalent macro function behavior
221
414
  - Compatible error handling
222
415
 
223
416
  All tests from the Java version have been ported to ensure equivalence.
224
417
 
418
+ ## Specification compliance
419
+
420
+ ```typescript
421
+ cel.eval('1 / 2', {}); // 0n, integer division truncates
422
+ cel.eval('1.0 / 0.0', {}); // Infinity, not an error
423
+ cel.eval('has(a.b)', data); // presence test, false rather than an error
424
+ cel.eval('type(42) == int', {}); // true
425
+ cel.eval('false && missing', {}); // false, errors are absorbed by a false operand
426
+ ```
427
+
428
+ Known deviations from the specification, kept deliberately:
429
+
430
+ - `uint` is represented as a signed 64-bit int, so `type(1u)` is `int` and `1u == 1` is true.
431
+ - `"ab" * 3` and `[1] * 2` repeat a string or list, and `"a" in "abc"` tests for a substring. CEL
432
+ defines none of these; they are extensions this library adds.
433
+ - `has(map, key)` is also accepted as a two-argument function alongside the `has(a.b)` macro.
434
+ - Map keys are not restricted to the specification's int, uint, bool and string types, and they are
435
+ matched with the same numeric equality the rest of the language uses. A key stored as `1`
436
+ matches a `1.0` probe, and `{1: "a"}[1.0]` resolves. Membership, indexing, presence tests and
437
+ map equality all follow this one rule, so `key in map ? map[key] : fallback` never fails after
438
+ its guard succeeds, and a `Map` bound from JavaScript compares equal to the literal that
439
+ spells it.
440
+ - Bytes are ordered the way the Java implementation orders them, byte by byte as signed values.
441
+
442
+ ### Evaluation limits
443
+
444
+ CEL exists to evaluate untrusted input, so operations whose size the expression itself controls are
445
+ bounded by `Utilities.LIMIT`, one million elements or characters. `lists.range(n)`, the repetition
446
+ operators and `format()` raise an `EvaluationError` rather than exhausting the heap:
447
+
448
+ ```typescript
449
+ cel.eval('lists.range(9223372036854775807)', {}); // EvaluationError
450
+ cel.eval("'x' * 2000000000", {}); // EvaluationError
451
+ cel.eval("'%.2000000f'.format([1.5])", {}); // EvaluationError
452
+ ```
453
+
454
+ The ceiling bounds each operation's **output**, not only its inputs, so two separately legal values
455
+ cannot be combined into one that exhausts the heap. `join()`, `replace()`, `regex.replace()`,
456
+ `base64.encode()` and string, list and bytes concatenation are all bounded this way:
457
+
458
+ ```typescript
459
+ cel.eval("lists.range(1000000).map(x, 'a').join('x' * 1000000)", {}); // EvaluationError
460
+ cel.eval("('x' * 1000000) + ('x' * 1000000)", {}); // EvaluationError
461
+ ```
462
+
463
+ Collection operations resolve membership through a hash index rather than a scan, so `distinct()`
464
+ and the `sets` functions stay linear in their input and cannot burn CPU quadratically at the size
465
+ the limit allows. The parser also refuses expressions nested more than 256 levels deep.
466
+
467
+ This is a ceiling on individual operations, not a budget for a whole expression: a deeply nested
468
+ expression can still allocate a multiple of the limit.
469
+
470
+ ## Breaking changes in 2.0.0
471
+
472
+ - CEL ints are now `bigint`: `cel.eval('1 + 2')` returns `3n`, and every int result (including
473
+ `size()`, indexes and `int()`) is a `bigint`. Integer-valued JavaScript numbers passed as
474
+ variables are ints; write `double(x)` in the expression to force a double. `JSON.stringify`
475
+ needs a replacer for `bigint`.
476
+ - Map literals and plain-object variables evaluate to `Map` rather than plain objects, and maps may
477
+ have non-string keys.
478
+ - Bytes literals now evaluate to `Uint8Array` instead of `string`. Use `string(value)` to decode
479
+ them as UTF-8, or `bytes(value)` to convert the other way.
480
+ - `type()` returns a `Type` value rather than a string, so `type(42) == int` is true and
481
+ `type(42) == "int"` is false. Use `string(type(x))` for the old string form. `type(1.0)` is
482
+ now `double`; previously any integer-valued number reported `int`.
483
+ - Integer division returns an integer: `1 / 2` is `0n`, not `0.5`. Use a double operand for the
484
+ old behavior. Double division by zero yields infinity instead of raising an error, and `%` now
485
+ requires integer operands.
486
+ - Integer arithmetic raises `EvaluationError` on 64-bit overflow instead of losing precision.
487
+ - Mixed int and double comparison is exact, so `9223372036854775807 == 9223372036854775808.0` is
488
+ false.
489
+ - `&&`, `||`, `!` and `? :` now require boolean operands instead of treating any non-true value as
490
+ false. The logical operators absorb errors: `false && error` is false and `true || error` is
491
+ true. Macro predicates must likewise yield a boolean.
492
+ - `has(a.b)` is a presence test resolved at parse time; `has(1)` is a `ParseError`. The two-argument
493
+ `has(map, key)` function is still supported.
494
+ - Timestamps and durations are new: `timestamp(n)` reads `n` as epoch **seconds**, results are
495
+ `Timestamp` and `Duration` instances (a `Date` is accepted as input), accessors default to UTC
496
+ and return `bigint`, and `string(duration)` renders the CEL form (`"3600s"`).
497
+ - String ordering compares UTF-16 code units (previously locale-aware `localeCompare`), and string
498
+ positions and sizes count code points rather than UTF-16 code units.
499
+ - `int('4.5')` and `double('abc')` are errors; the conversions no longer accept partial numbers.
500
+ - `null` and `undefined` variables both evaluate to CEL `null`.
501
+ - Function argument errors throw the new `ArgumentError` rather than a plain `Error`;
502
+ `EvaluationError` is unchanged. Both are exported.
503
+ - `Utilities` signatures changed: `typeOf` returns `Type`, `sizeOf`, `asInt`, `asUInt` and the
504
+ time accessors return `bigint`, `deepEquals` is an alias of `equals`, and `compare` no longer
505
+ accepts `null`.
506
+ - The `Functions` interface gained an optional `knows(name)` method for namespaced functions, and
507
+ results returned from custom functions are normalised into the CEL value model.
508
+ - An integer literal outside the 64-bit range is a `ParseError`.
509
+
225
510
  ## Requirements
226
511
 
227
- - Node.js 18+ or modern browser
512
+ - Node.js 18+ or a modern browser (BigInt, `TextEncoder` and `Intl.DateTimeFormat` with time
513
+ zone support)
228
514
  - TypeScript 5.0+ (for development)
229
515
 
230
516
  ## API Documentation
@@ -235,10 +521,14 @@ All tests from the Java version have been ported to ensure equivalence.
235
521
  class CEL {
236
522
  constructor(functions?: Functions | null);
237
523
  compile(expression: string): Program;
238
- eval(expression: string, variables?: Record<string, any>): any;
524
+ eval(expression: string, variables?: Record<string, unknown> | Map<string, unknown>): unknown;
239
525
 
240
526
  static compile(expression: string, functions: Functions): Program;
241
- static eval(expression: string, functions: Functions, variables: Record<string, any>): any;
527
+ static eval(
528
+ expression: string,
529
+ functions: Functions,
530
+ variables: Record<string, unknown> | Map<string, unknown>
531
+ ): unknown;
242
532
  }
243
533
  ```
244
534
 
@@ -246,7 +536,7 @@ class CEL {
246
536
 
247
537
  ```typescript
248
538
  class Program {
249
- evaluate(variables?: Record<string, any>): any;
539
+ evaluate(variables?: Record<string, unknown> | Map<string, unknown>): unknown;
250
540
  }
251
541
  ```
252
542
 
@@ -256,15 +546,36 @@ class Program {
256
546
  interface Functions {
257
547
  callFunction(name: string, args: any[]): any;
258
548
  callMethod(target: any, method: string, args: any[]): any;
549
+ knows?(name: string): boolean;
550
+ }
551
+ ```
552
+
553
+ ### Printer
554
+
555
+ ```typescript
556
+ class Printer {
557
+ static print(expr: Expression, options?: PrinterOptions): string;
558
+ }
559
+
560
+ class PrinterOptions {
561
+ constructor(wrap: boolean, indent: number, width: number);
562
+ static compact(): PrinterOptions;
563
+ static pretty(): PrinterOptions;
259
564
  }
260
565
  ```
261
566
 
567
+ ### Values
568
+
569
+ `Type`, `Timestamp`, `Duration`, `EvaluationError` and `ArgumentError` are exported, as are the
570
+ `Utilities`, `Strings`, `Lists`, `Sets`, `Maths`, `Regexes` and `Codecs` namespaces for calling the
571
+ library functions directly.
572
+
262
573
  ## Examples
263
574
 
264
575
  See the [examples](./examples) directory for more detailed examples:
265
576
 
266
577
  - [quickstart.ts](./examples/quickstart.ts) - Comprehensive usage examples
267
- - [parser-example.ts](./examples/parser-example.ts) - Parser API demonstration
578
+ - [parser-example.ts](./examples/parser-example.ts) - Parser and Printer API demonstration
268
579
  - [interpreter-example.ts](./examples/interpreter-example.ts) - Interpreter API demonstration
269
580
 
270
581
  ## License