The language
cel-lite runs a subset of CEL, the Common Expression Language. An expression means what CEL's language definition says it means. What cel-lite doesn't implement it refuses, by name, when the expression is compiled: an expression that compiles never means something other than it would in CEL.
cel-go, CEL's reference
implementation, is the oracle. The tests in oracle/ run the same
expressions through both engines and compare types, values, errors and
unknowns: a corpus of corner cases, thousands of generated expressions on
random variables, and two fuzz targets. Where cel-go strays from the
definition, cel-lite follows the definition, and the difference is listed
below.
Types
| Type | Values | Go |
|---|---|---|
bool |
true, false |
bool |
int |
64-bit signed integers | int64 |
uint |
64-bit unsigned integers | uint64 |
double |
64-bit floating point | float64 |
string |
Unicode text | string |
bytes |
byte sequences | []byte |
list(T) |
[1, 2, 3] |
a slice, or a List |
map(K, V) |
{'a': 1}; keys bool, int, uint, string |
a map, or a Map |
null_type |
null |
nil |
| a struct | a struct your program declares | an Object |
dyn |
any of the above, checked as it runs |
Every expression is type-checked when it compiles. A value of type dyn
is checked when the expression runs instead: dyn(x) makes any value
dyn, and a variable declared dyn holds anything.
Literals
42 -7 0x1F int
42u 0x1Fu uint
1.5 1e3 .5 1.e3 double
'single' "double" string, with \n \t \\ \' \" \xHH \uHHHH escapes
'''triple''' """too""" string, across lines
r'raw \n' string, no escapes
b'bytes\xff' bytes
true false null
[1, 2, 3] {'a': 1} list, map; a trailing comma is allowed
Operators
From the loosest to the tightest:
| Operators | Meaning |
|---|---|
c ? a : b |
a if c, else b; c is a bool |
|| |
or |
&& |
and |
== != < <= > >= in |
equality, order, membership |
+ - |
add, subtract, concatenate |
* / % |
multiply, divide, remainder |
! - |
not, negate |
a.f a[i] f(x) a.f(x) |
select, index, call, method call |
- Arithmetic takes two values of one type:
1 + 1uis a type error. Overflow inintoruintis an error when the expression runs, and so is dividing an integer by zero.doublefollows IEEE 754:1.0 / 0.0is infinity, and NaN equals nothing, itself included. Integer division truncates toward zero:-5 / 3is-1, and-5 % 3is-2. +also concatenates strings, bytes and lists.- Equality and order take two values of one type when their types
are known:
1 == 1uis a type error, as in cel-go. Values of type dyn compare across the numeric types by their value:dyn(1) == 1uanddyn(1) < 1.5hold. Lists and maps are equal when their elements are. Strings order by code point, bytes by byte, andfalse < true. inis membership: an element of a list, or a key of a map.c ? a : bevaluates only the branch it takes.
Names and fields
- A variable is declared by your program, with its type. Its name
may be qualified: a variable declared as
pane.idis whatpane.idreads, even wherepaneis a variable too. A leading dot,.pane.id, names it from the root. a.fis a struct's field, or a map's value at the key'f'.a[k]is a list's element (at anint) or a map's value. A missing key or an index out of range is an error when the expression runs.has(a.f)is whether the mapaholds the key'f'.- The words CEL reserves (
in,if,null,true,package, …) can't name a variable or a helper, but a field may have one:x.if.cellite.ReservedWords()lists them. - A variable may take the name of a type (
list,int,type), and the name then reads the variable. Without such a variable, a type's name in an expression is a type value, which cel-lite refuses.
Functions
| Function | Meaning |
|---|---|
size(x), x.size() |
a string's code points, bytes' length, a list's or a map's entries |
s.contains(t) |
whether s holds t |
s.startsWith(t), s.endsWith(t) |
whether s begins, or ends, with t |
int(x) |
from a uint, a double (toward zero) or a string |
uint(x) |
from an int, a double or a string |
double(x) |
from an int, a uint or a string |
string(x) |
from a number, a bool, or bytes that are UTF-8 |
bytes(x) |
a string's UTF-8 |
bool(x) |
from 'true', 'TRUE', 'True', 't', '1', and the same for false |
dyn(x) |
x, of type dyn |
A conversion that doesn't fit, int(1e19) or int('ten'), is an error
when the expression runs. So is one whose argument isn't of the type
Check gave it, as untyped data can give a variable: int(s) with s a
string variable given an int, as in cel-go. Your program can add functions of its own:
helpers.
Errors and unknowns
An expression that compiles can still fail as it runs: a division by zero, an overflow, a missing key, a variable the activation doesn't have. A variable can also be unknown: its value isn't there yet, as in partial evaluation.
An error or an unknown is the result of whatever depends on it, except
that && and || look at both sides, in either order:
false && xandx && falsearefalse, andtrue || xandx || truearetrue, whateverxis: an error, an unknown, or a value.- When neither side decides, an unknown wins over an error: with
xunknown,x && 1 / 0 == 1is unknown. c ? a : bis unknown or an error whencis.
What cel-lite refuses
Each of these is refused when the expression compiles, with an
*cellite.Error whose Kind is Unsupported and whose Name names the
feature:
| Feature | Example | Error.Name |
|---|---|---|
| Comprehension macros | tags.exists(t, t == 'x') |
macro exists, macro all, … |
| Regular expressions | name.matches('^a') |
function matches |
| Timestamps and durations | timestamp('2026-01-01T00:00:00Z') |
function timestamp, function duration, function getHours, … |
| Type values | type(x) == int |
function type, type int |
| Message construction | Item{title: 'a'} |
message construction |
| Optional syntax | a.?b, [?x], {?'k': v} |
optional syntax |
| Escaped identifiers | a.`b-c` |
escaped identifier |
has() on a struct's field |
has(item.title) |
has() on a struct |
Comparing structs (not with null) |
item == other |
== on structs |
| Map keys of other types | {1.5: 'a'} |
map key double |
| Inferred types | -x['k'] with x dyn |
inferred type |
The macros are all, exists, exists_one, map and filter. The last row is explained below.
A dyn can hide a struct from the checker: x == y with both dyn, or
has(x.f). When the values turn out to be structs, the refusal comes as
the expression runs, as an *EvalError that says it is not supported,
unless the answer doesn't depend on it: [x, 1] == [y, 2] is false,
and 'a' in [x, 'a'] is true.
Where cel-lite and cel-go differ
Where cel-go strays from the language definition, cel-lite follows the definition, or refuses by name. The comparison with cel-go leaves out each of these cases by name.
- Inferred types. cel-go types a value read with
[]from a dyn (x['k'], or an element of an empty literal) with a type parameter that its first use binds:-x['k']is a double there,[x['k'], 'a']alist(string),-x['k'] + 1is refused, andint(x['k'])takes only an int when it runs. In the definition the value is dyn. cel-lite refuses such a use (inferred type);x.kordyn(x['k'])keeps the value dyn. - Map keys. A map's keys are bool, int, uint or string, as the definition says. A map literal whose dyn key turns out a double, a list or a map is an error when it runs; cel-go builds the map, where a NaN key is never found and two NaN keys are two entries.
- A variable named after a type, absent. When a variable named
listorintis declared but the activation doesn't have it, cel-go reads the type instead, a value the expression's checked type doesn't allow:listdeclared a bool gives the typelist. cel-lite reports the variable missing, as for any other name. []on a struct. cel-go reads a struct's field by its name with[]when a dyn hides the struct:dyn(item)['title']. In the definition[]reads a list or a map, and a field is read with.; cel-lite gives an error (no such overload).- A bytes key.
x in mwithxbytes, as untyped data can give a variable declared a string, isfalseon any map: bytes are never a key. cel-go fails on a map literal, which can't hash them, and saysfalseon a Go map. - Limits. cel-lite reads shorter and shallower expressions than cel-go: see Limits.
Error messages are cel-lite's own. Type errors read as cel-go's do
(found no matching overload for '_+_' applied to '(string, int)'), and
so do undeclared names; syntax errors are worded differently
(syntax error: unexpected end of input).