Introduction
This book is a tutorial and reference for CEL: its tokens, literals, basic types, operators, and collections.
Every CEL source form is an expression: it produces a value or helps compose one. Identifiers, calls, operators, collections, control flow, casts, and closures all participate in the same expression language.
The tutorial chapters follow this order:
- Lexical conventions explains tokens, whitespace, comments, identifiers, and punctuation.
- Literals and types explains scalar, compound, and closure values.
- Operators explains precedence, associativity, arithmetic, comparison, logical, bitwise, cast, and range operators.
- Conditional expressions explains
value-producing
ifexpressions. - Collections explains tuples and homogeneous arrays.
- Closures explains closure literals and parameter typing.
- Standard library explains additional operations available when the evaluation environment installs the library.
The Reference Manual gathers the complete grammar, exact operator rules, type names, library operations, and edge cases in one place.
Lexical conventions
The lexer tokenizes a stream of characters into tokens before the parser reads them as expressions. It is built on Rust token trees and inherits many behaviors from the Rust lexical structure. This chapter describes the token forms that participate in CEL source. For the full CEL expression grammar, see the Reference Manual.
Syntax
compound_token = "&&" | "||" | "==" | "!=" | "<=" | ">="
| "<<" | ">>" | ".." | "..=" .
identifier = identifier_start { identifier_continue } .
Whitespace and Compound tokens
Spaces, tabs, and line breaks separate tokens. They do not otherwise change the meaning of an expression.
x + 1
x+1
if flag {
1
} else {
2
}
Certain punctuation characters combine to form single tokens so long as there is no intervening white space. This includes, &&, ||,
==, !=, <=, >=, <<, >>, .., and ..=.
a&&b
x<=y
1..=5
Comments and trivia
Comments are source trivia. They may appear between tokens in the same places as whitespace.
x /* midpoint */ + 1
if ready { 1 } // done
else { 0 }
Line comments use // .... Block comments use /* ... */.
Identifiers and reserved words
An identifier is a non-empty name made from Unicode identifier characters.
The first character may be a Unicode letter or _; later characters may also
include Unicode digits. CEL follows the Rust
identifier rules for
the spellings that its lexer accepts.
x
user_name
value2
π
_temporary
These words are reserved for fixed roles in CEL source and cannot name user-defined values:
ifelseastruefalse
true and false are boolean literals. if and else introduce conditional
expressions. as introduces an explicit cast.
Literal tokens
The lexer recognizes these literal token categories:
- integer literals
- floating-point literals
- string literals
- boolean literals
- character literals
- byte literals
- byte-string literals
- C-string literals
42
3.5
"hello"
true
'a'
b'A'
b"bytes"
c"header"
The grammar describes the unit value () rather than a standalone literal
token.
Delimiters and punctuation
CEL uses three delimiter pairs:
()for grouping, unit, tuples, calls, and closure parameter types[]for arrays{}forifbranches
It uses these structural punctuation marks inside expressions:
,between tuple elements, array elements, and call arguments:in typed closure parameters.before an unsuffixed tuple index
(1, 2)
[0, 1, 2]
|x: i32| x + 1
point.0
Operators
Operator tokens carry arithmetic, comparison, logical, bitwise, cast, and range syntax.
+ - * / %
== != < <= > >=
! && ||
& | ^ << >>
as
.. ..=
The grammar determines where each operator may appear and how tightly it binds. See Reference Manual for the full expression grammar.
Literals and types
CEL source includes literal values for numbers, booleans, text, characters, and bytes. It also uses tuple, array, range, and closure syntax to build larger values from expressions. For the full grammar of these forms, see the Reference Manual.
Built-in scalar type names
These sixteen names are the built-in scalar type names used by as casts and
typed closure parameters.
| Category | Types |
|---|---|
| Signed integers | i8, i16, i32, i64, i128, isize |
| Unsigned integers | u8, u16, u32, u64, u128, usize |
| Floating-point numbers | f32, f64 |
| Other scalars | bool, String |
The language also accepts character, byte-string, C-string, and unit values in expressions. The named scalar type set remains the sixteen type names listed above.
Syntax
literal = integer_literal
| floating_point_literal
| boolean_literal
| string_literal
| character_literal
| byte_literal
| byte_string_literal
| c_string_literal .
Integer literals
An integer literal is a whole number optionally followed by one of the accepted integer suffixes.
| Suffix | Resulting type |
|---|---|
| none | i32 |
i8 | i8 |
i16 | i16 |
i32 | i32 |
i64 | i64 |
i128 | i128 |
isize | isize |
u8 | u8 |
u16 | u16 |
u32 | u32 |
u64 | u64 |
u128 | u128 |
usize | usize |
Unary - combines with a numeric literal to form a negative numeric
expression.
0
42i8
42i64
42u16
42u128
42usize
-1i32
Floating-point literals
A floating-point literal uses decimal or exponent notation, optionally followed by a floating-point suffix.
| Suffix | Resulting type |
|---|---|
| none | f64 |
f64 | f64 |
f32 | f32 |
3.5
1e3
0.25f64
6.022e23f32
-3.5
Boolean, string, character, byte, and unit values
Boolean literals are true and false. String literals produce String
values. Character literals produce char values. Byte literals produce u8
values. Byte-string and C-string literals produce byte-oriented string values.
Write the unit value as ().
true
false
"hello"
'a'
b'A'
b"bytes"
c"header"
()
A byte literal such as b'A' and an explicitly suffixed integer such as 65u8
both denote u8 values.
Compound values
CEL also builds compound values from ordinary expressions:
()is the unit value,(expr)is grouping,(expr,)is a 1-tuple, and(a, b, c)is a tuple.- Arrays are homogeneous bracketed lists. An unannotated array is non-empty;
a type annotation such as
[]: [i32]supplies the element type for an empty array. || exprand|x: T| exprare closure values.- These forms can nest inside calls, tuples, arrays, conditionals, and one another wherever the grammar permits.
- Array homogeneity compares complete recursive shapes, including every tuple
field and callable parameter/result.
[]: [(i32, [f64])]and[]: [Fn((i32,), [i32])]need no registration of those aggregate shapes.
(1,)
(1, 2, 3)
[0, 1, 2]
[0, 1]: [i32]
[]: [i32]
|x: i32| x + 1
Source () and the empty tuple type are one canonical type, witnessed by Nil
and represented by CNil<()>. Bridge ordinary Rust () with the zero-element
IntoCStackList / IntoRustTuple conversion; it is not a separate source leaf
that merely shares a zero-sized layout. An explicitly registered opaque host
Leaf<()> remains a distinct host identity.
#![allow(unused)]
fn main() {
/// Normalizes source unit to the zero-element list and bridges it to Rust unit.
#[test]
fn source_unit_is_an_empty_list() {
use cel_parser::{CELParser, OpLookup};
use cel_runtime::{CNil, IntoCStackList, IntoRustTuple, Nil};
let body = CELParser::new(OpLookup::new()).parse_str("()").unwrap();
let mut body = body.prepare::<Nil, Nil>().unwrap();
let unit: CNil<()> = body.call(&().into_c_stack_list()).unwrap();
let (): () = unit.into_rust_tuple();
}
}
Host-registered records
A host can register a named record schema and operations without changing CEL.
Write Sample { value: 3.0, } to construct a record when the host supplies that
schema. Field labels select schema fields rather than variables; values evaluate
in written order. Supply every field exactly once. Empty schemas accept {},
and non-empty field lists accept a trailing comma. CEL reports unknown
constructors, unknown/duplicate/missing fields, and mismatched field types.
Records use exact scalar types: 1 is i32, never implicitly f64; use 1.0
or an explicit supported cast for an f64 field. Constructor names occupy a
separate namespace from values, so a value named Sample does not hide the
constructor. The host must separately register any operation such as .value;
a record schema does not automatically create accessors.
This executable embedding example registers a real Rust value and accessor.
Both bare and parenthesized member spellings return 3.0; calling that numeric
result with (record.value)() fails.
#![allow(unused)]
fn main() {
/// Evaluates a host record and its direct accessor with exact field types.
#[test]
fn registered_record_and_member_example() {
use cel_parser::{OperationSignature, RecordField, RecordSchema, ResolvedLeafType};
use cel_runtime::ValueType;
/// A host-defined value, not a built-in CEL type.
#[derive(Clone, Debug, PartialEq)]
struct Sample {
value: f64,
}
let sample = ResolvedLeafType::new("Sample", ValueType::leaf::<Sample>());
let scalar = ResolvedLeafType::new("f64", ValueType::leaf::<f64>());
let mut lookup = OpLookup::new();
lookup
.register_record(RecordSchema {
ty: sample.clone(),
fields: vec![RecordField {
name: "value",
ty: scalar.clone().into(),
}],
emit: Box::new(|s, _, _, _| s.op1(|value: f64| Sample { value })),
})
.unwrap();
lookup
.register_operation(OperationSignature {
name: "value",
operands: vec![sample.into()],
result: scalar.into(),
emit: Box::new(|s, _| s.op1(|sample: Sample| sample.value)),
})
.unwrap();
let mut parser = CELParser::new(lookup);
for source in [
"Sample { value: 3.0, }.value",
"Sample { value: 3.0 }.value()",
] {
assert_eq!(
parser.parse_str(source).unwrap().call0::<f64>().unwrap(),
3.0
);
}
assert_eq!(
parser
.parse_str("if (Sample { value: 3.0 }.value) > 0.0 { 1 } else { 0 }")
.unwrap()
.call0::<i32>()
.unwrap(),
1
);
assert!(parser.parse_str("Sample { value: 1 }").is_err());
assert!(parser.parse_str("(Sample { value: 3.0 }.value)()").is_err());
assert!(
parser
.parse_str("Sample { value: 3.0 }.value = 4.0")
.is_err()
);
}
}
Range values
A range expression produces a range value from zero, one, or two numeric endpoints. CEL supports bounded, half-bounded, and unbounded forms:
a..b
a..=b
a..
..b
..=b
..
Endpoint-bearing ranges require homogeneous numeric endpoints. Each endpoint is a full expression, so operators inside either side are parsed before the range is formed. The Operators chapter defines precedence and the non-chaining rule for range expressions.
Use this chapter for value categories and Reference Manual for the complete grammar of literal, tuple, array, range, and closure syntax.
Operators
CEL operators combine existing expression values. Precedence and associativity determine how unparenthesized expressions group. For the full grammar, see the Reference Manual.
Concept
Unary, binary, cast, and postfix operators all participate in one expression grammar. Postfix calls, tuple indices, and named members bind most tightly. Range forms bind most loosely. Logical operators short-circuit, and comparison and range forms do not chain.
Syntax
range_expression = ".." [ or_expression ]
| "..=" or_expression
| or_expression [ ".." [ or_expression ] | "..=" or_expression ] .
or_expression = and_expression { "||" and_expression } .
and_expression = comparison_expression { "&&" comparison_expression } .
comparison_expression = bitwise_or_expression
[ ("==" | "!=" | "<" | ">" | "<=" | ">=") bitwise_or_expression ] .
bitwise_or_expression = bitwise_xor_expression { "|" bitwise_xor_expression } .
bitwise_xor_expression = bitwise_and_expression { "^" bitwise_and_expression } .
bitwise_and_expression = bitwise_shift_expression { "&" bitwise_shift_expression } .
bitwise_shift_expression = additive_expression { ("<<" | ">>") additive_expression } .
additive_expression = multiplicative_expression { ("+" | "-") multiplicative_expression } .
multiplicative_expression = cast_expression { ("*" | "/" | "%") cast_expression } .
cast_expression = unary_expression { "as" identifier } .
unary_expression = (("-" | "!") unary_expression) | postfix_expression .
postfix_expression = primary_expression {
"(" [ argument_list ] ")" | "." (unsuffixed_integer | identifier [ "(" [ argument_list ] ")" ])
} .
argument_list = expression { "," expression } .
Worked examples
10 + 20 * 5
"cel" + "-" + "lang"
ok && round_ties_even(3.5) > 3.0
1u64 << 3u32
1.5 as i32
1 + 2..3 * 4
Exact rules
Precedence runs from lowest to highest in this order:
- range forms
||&&- comparison operators
- bitwise
| - bitwise
^ - bitwise
& - shifts
<<and>> - additive
+and- - multiplicative
*,/, and% - casts with
as - unary
-and! - postfix calls, tuple indexing, and named members
Within one precedence level, binary operators associate to the left. Comparison expressions consume one comparison operator, and range forms do not chain.
- A call is a postfix
()form with zero or more comma-separated arguments. receiver.nameandreceiver.name()both invoke a unary named operation, ignoring same-named value bindings.receiver.name(a, b)supplies the receiver first, then explicit arguments; each evaluates once in source order. The member label is not a variable reference, even inside a closure.- Grouping changes call semantics:
(receiver.name)()calls the accessor’s result as a value. It is not another spelling ofreceiver.name(). Named members, calls, and.Nshare postfix precedence and chain left to right. Neither named members nor tuple indices support assignment. - Hosts supply member signatures, not a fixed set of receiver types. The checker rejects known incompatible operands and preserves registered result types. Calls without metadata remain best-effort and still check every argument.
- Postfix calls and tuple indices bind more tightly than unary and binary operators.
+accepts homogeneous numeric operands andString + String. Unsigned integer addition wraps. Signed integer addition reportsarithmetic overflowon overflow.- Binary
-accepts homogeneous numeric operands. Unary-accepts only signed integers and floating-point values. Unsigned integer subtraction wraps. Signed binary subtraction and signed unary negation reportarithmetic overflowon overflow. *accepts homogeneous numeric operands. Unsigned integer multiplication wraps. Signed integer multiplication reportsarithmetic overflowon overflow./and%accept homogeneous numeric operands. Floating-point division and remainder use normal floating-point behavior. Integer failures reportdivision by zero.&,|, and^accept homogeneous integer operands only.<<and>>accept integer operands only. The right operand must fit inu32, and the shift count must be in range. Shift failures reportshift overflow.!acceptsboolonly.&&and||acceptbooloperands only and short-circuit.==and!=accept homogeneous numeric operands, homogeneousbool, or homogeneousString.<,<=,>, and>=accept homogeneous numeric operands or homogeneousString.- Range forms bind more loosely than every other operator. The valid forms
are
a..b,a..=b,a..,..b,..=b, and... ..=always requires a right endpoint.- Range endpoints are full expressions.
- Range expressions do not chain, so
1..2..3is not valid. 1 + 2..3 * 4means(1 + 2)..(3 * 4).asconverts a value to one of the sixteen built-in scalar type names:i8,i16,i32,i64,i128,isize,u8,u16,u32,u64,u128,usize,f32,f64,bool, andString.- Integer targets accept integer, floating-point, and
boolsources. Floating-point targets accept integer and floating-point sources.boolandStringtargets accept only their same-type sources. - Integer-to-integer casts check that the source fits in the target.
Floating-point-to-integer casts require a finite, in-range source and
truncate toward zero.
f64 as f32checks finite range before narrowing. truecasts to integer1, andfalsecasts to integer0.- CEL limits cast targets to the sixteen built-in scalar type names.
char, byte-string, C-string, unit, array, tuple, range, and closure values are not additional cast target names.- Number-to-
bool,bool-to-float, andString-to-number orString-to-boolcasts are not valid. - Casts associate from left to right, so
x as T as Uapplies the first cast before the second. - See Conditional expressions for
ifexpressions, and the Reference Manual for a summary.
Edge cases
- Write
a < b && b < c, nota < b < c. - Call argument lists do not accept a trailing comma.
- Negative shift counts, counts larger than
u32, and counts outside the left operand’s width reportshift overflow. - Signed
+, signed binary-, signed unary-, and signed*overflows reportarithmetic overflow. - Integer
/and%reportdivision by zerowhen they fail. - Casts cannot target
char, byte-string, C-string, unit, array, tuple, range, or closure values.
Conditional expressions
CEL uses value-producing if expressions to direct expression flow. For the
exact grammar, see the
Reference Manual.
Concept
An if expression selects one branch value. It is an ordinary expression and
can appear anywhere an expression is expected.
Syntax
if_expression = "if" expression "{" expression "}"
[ "else" ( "{" expression "}" | if_expression ) ] .
Worked examples
if ready { 1 } else { 0 }
if score > 90 { "high" } else if score > 75 { "mid" } else { "low" }
Exact rules
ifis an expression, not a statement. It yields the value of the selected branch.- CEL supports the branch forms
if,if ... else, andif ... else if ...chains. - The condition and every branch body are ordinary expressions.
- Omitting the final
elsesupplies an implicit()branch, so the remaining branches must still be type-compatible with unit. - The braces belong to
ifsyntax. CEL does not use free-standing block expressions.
Edge cases
if flag { () } else if other { () }is valid without a finalelsebecause the omitted branch is also().- See the Reference Manual for the complete branch grammar.
Collections
CEL uses tuples and arrays for compound values. Tuples preserve fixed positional shape. Arrays hold homogeneous sequences of values; an empty array uses a type annotation to state its element type. For the complete grammar, see the Reference Manual.
Concept
Parentheses and brackets build different kinds of values. Parentheses can
mean unit, grouping, or tuples. Brackets always mean arrays. That shape
difference matters because tuple indexing uses .N, while arrays follow
their own literal rules and do not use tuple indexing syntax.
Syntax
tuple_or_group = "(" [ expression [ "," [ expression { "," expression } ] ] ] ")" .
array_expression = "[" [ expression { "," expression } ] "]"
[ ":" type_expr ] .
type_expr = identifier [ "(" type_expr { "," type_expr } ")" ]
| "[" type_expr "]"
| "(" [ type_expr [ "," [ type_expr { "," type_expr } ] ] ] ")" .
Worked examples
(1,)
(1, 2, 3)
[0, 1, 2]
[[0], [1]]
[0, 1]: [i32]
[]: [i32]
[]: [[i32]]
[(7, [1.5]), (8, []: [f64])]
[]: [(i32, [f64])]
[]: [Fn((i32,), [i32])]
if ready { [1, 2] } else { [3, 4] }
Exact rules
()is unit,(expr)is grouping,(expr,)is a 1-tuple, and(a, b, c)is a tuple.[a, b, c]is an array literal.- An unannotated array is non-empty and every element must have the same type.
- A type annotation after
:states the complete array type. It permits an empty array, as in[]: [i32]. - A non-empty array annotation must match the element type exactly.
- Nested arrays are valid when each element is itself an array of the same element type.
- Tuples and arrays are distinct value forms: tuples are positional and
use
.N, while arrays use bracket literals and homogeneous element typing. - Tuple indexing uses an unsuffixed integer such as
.0or.1. Named postfixes such asvalue.nameinvoke host-provided operations; they do not select tuple fields. - Arrays and tuples compose recursively over registered leaves, unit, other containers, and callables. Callable element types include their complete parameter and result shapes; storing a callable does not require equality.
Rust interoperability
The recursive mapping uses concrete values, not per-element erased owners:
| CEL shape | Rust value | Representation witness |
|---|---|---|
registered leaf T | T | Leaf<T> |
[T] | Array<R(T)> | ArrayOf<W(T)> |
() | CNil<()> | Nil |
(T, U) | CStackList<R(T), CStackList<R(U), CNil<()>>> | Cons<W(T), Cons<W(U), Nil>> |
Fn((A,), R) | Callable<CStackList<R(A), CNil<()>>, R(R)> | FnOf<Cons<W(A), Nil>, W(R)> |
Here R(T) is the recursively canonical value and W(T) its witness.
IntoCStackList and IntoRustTuple move between lists and ordinary Rust tuples
without requiring Clone, equality, or formatting. Logical list order is source
order even though the repr(C) cons cell physically stores its tail first.
Never cast native Rust tuple bytes to a list.
This executable example prepares an array of actual lists, then moves each row into a native Rust tuple:
#![allow(unused)]
fn main() {
/// Moves concrete tuple-list elements out of an interpreted array in source order.
#[test]
fn arrays_of_tuple_lists() {
use cel_parser::{CELParser, OpLookup};
use cel_runtime::{ArrayOf, Cons, IntoCStackList, IntoRustTuple, Leaf, Nil};
type Row = Cons<Leaf<i32>, Cons<ArrayOf<Leaf<f64>>, Nil>>;
let body = CELParser::new(OpLookup::new())
.parse_str("[(7, [1.5, 2.5]), (8, []: [f64])]")
.unwrap();
let mut body = body.prepare::<Nil, ArrayOf<Row>>().unwrap();
let rows = body.call(&().into_c_stack_list()).unwrap();
let actual: Vec<_> = rows
.into_vec()
.into_iter()
.map(|row| {
let (id, values) = row.into_rust_tuple();
(id, values.into_vec())
})
.collect();
assert_eq!(actual, [(7, vec![1.5, 2.5]), (8, vec![])]);
}
}
Nested vectors and slices
Array<T>::from_vec and into_vec transfer the allocation in O(1);
as_slice borrows the same elements as &[T]. They do not reinterpret Rust’s
unspecified Vec<T> header.
The representation of [[i32]] is Array<Array<i32>>. Its allocation-preserving
vector view is Vec<Array<i32>>, and its slice view is &[Array<i32>], not
Vec<Vec<i32>> or &[Vec<i32>]. An explicit deep conversion builds a new outer
vector while transferring each inner allocation independently:
#![allow(unused)]
fn main() {
/// Transfers the outer allocation to a vector of canonical inner arrays.
#[test]
fn nested_array_vector_view() {
use cel_parser::{CELParser, OpLookup};
use cel_runtime::{Array, ArrayOf, IntoCStackList, Leaf, Nil};
let body = CELParser::new(OpLookup::new())
.parse_str("[[1, 2], []: [i32], [3]]")
.unwrap();
let mut body = body.prepare::<Nil, ArrayOf<ArrayOf<Leaf<i32>>>>().unwrap();
let rows: Array<Array<i32>> = body.call(&().into_c_stack_list()).unwrap();
let pointer = rows.as_slice().as_ptr();
let capacity = rows.capacity();
let rows: Vec<Array<i32>> = rows.into_vec();
assert_eq!(rows.as_ptr(), pointer);
assert_eq!(rows.capacity(), capacity);
assert_eq!(rows[0].as_slice(), [1, 2]);
assert!(rows[1].is_empty());
assert_eq!(rows[2].as_slice(), [3]);
// Deep conversion moves each inner allocation, but builds a new outer vector.
let native: Vec<Vec<i32>> = rows.into_iter().map(Array::into_vec).collect();
assert_eq!(native, [vec![1, 2], vec![], vec![3]]);
}
}
One operation for native and interpreted callers
op1_repr binds complete canonical witnesses, op1_vec moves an array into an
owned vector for a callback, and op1_slice borrows its elements for the callback
without consuming or rebuilding their allocation to obtain the borrow. The
operation still owns its operand and drops it after the slice callback.
The following ordinary Rust function serves both native construction and CEL.
register_function binds its ordinary-call name; register_operation supplies
the complete signature and emitter:
#![allow(unused)]
fn main() {
use cel_runtime::{Array, CNil, CStackList};
/// Names the actual Rust value of a CEL `(i32, bool)`, not its witness.
type RowValue = CStackList<i32, CStackList<bool, CNil<()>>>;
/// Reverses rows without cloning their elements.
///
/// - Complexity: O(n) time for n rows; preserves the vector allocation.
fn reverse_rows(mut rows: Vec<RowValue>) -> Array<RowValue> {
rows.reverse();
Array::from_vec(rows)
}
/// Uses one concrete Rust operation from native construction and interpreted CEL.
#[test]
fn shared_native_and_interpreted_operation() {
use cel_parser::{CELParser, OpLookup, OperationSignature, builtin_type_resolver};
use cel_runtime::{ArrayOf, Cons, IntoCStackList, IntoRustTuple, Leaf, Nil};
type Row = Cons<Leaf<i32>, Cons<Leaf<bool>, Nil>>;
let native = reverse_rows(vec![
(7, true).into_c_stack_list(),
(8, false).into_c_stack_list(),
]);
let shape = CELParser::new(OpLookup::new())
.parse_type_expr_str("[(i32, bool)]")
.unwrap()
.resolve(builtin_type_resolver().as_ref())
.unwrap();
let mut lookup = OpLookup::new();
/// Binds the operation's ordinary-call name separately from its signature.
#[derive(Clone)]
struct ReverseRows;
lookup
.register_function("reverse_rows", ReverseRows)
.unwrap();
lookup
.register_operation(OperationSignature {
name: "reverse_rows",
operands: vec![shape.clone()],
result: shape,
emit: Box::new(|body, _| body.op1_vec::<Row, ArrayOf<Row>, _>(reverse_rows)),
})
.unwrap();
let body = CELParser::new(lookup)
.parse_str("reverse_rows([(7, true), (8, false)])")
.unwrap();
let mut body = body.prepare::<Nil, ArrayOf<Row>>().unwrap();
for rows in [native, body.call(&().into_c_stack_list()).unwrap()] {
let actual: Vec<_> = rows
.into_vec()
.into_iter()
.map(IntoRustTuple::into_rust_tuple)
.collect();
assert_eq!(actual, [(8, false), (7, true)]);
}
}
}
This is native Rust construction, not a macro-compiled CEL backend.
cel-rs-macros validates CEL at compile time; it does not generate native CEL.
Checked boundaries and prepared execution
ValueType records complete identity, layout, ownership, and optional
capabilities. ResolvedType and the checker type Ty retain recursive shapes,
including callable signatures. Equal sizes or shared carrier IDs cannot prove
identity, even for empty arrays.
OwnedValue::try_into_typed checks arbitrary external ownership against a full
witness. DynSegment::prepare::<Args, Result> instead checks a typed frame once
and produces PreparedSegment; its repeated call borrows canonical arguments
and returns independent owners without descriptor comparisons, downcasts, or
per-element validation. Hosts with runtime-bound storage can use
prepared_segment::PreparedRawSegment: construction checks descriptors, but
its unsafe call requires exact, live, aligned pointers for that prepared frame.
Neither API removes value-dependent bounds checks or body errors.
Clone, equality, and debug formatting are separate capabilities. Construction and moving ownership do not require them; borrowing an emitted argument read requires cloning, and requesting comparison or formatting requires the matching capability. The examples use built-in capabilities, not an implicit bound on all CEL values.
Edge cases
[]is not valid because an empty array needs a type annotation.[0,]is not valid.[]: [i32]is a valid typed empty array.[0, true]is not valid because the element types differ.[(0, 1)]and[]: [(i32, i32)]are valid tuple arrays.[]: [[i32]]is a valid typed empty nested array.[(0,), (true,)]is invalid because the complete element shapes differ.- Bracket postfix indexing such as
items[0]is not part of the language. - A suffixed tuple index such as
.0i32is not valid.
Closures
CEL uses |...| for closure literals. A closure is an expression and can
appear anywhere an expression is expected. For the exact grammar, see the
Reference Manual.
Concept
A closure packages a parameter list and a single expression body into a callable value.
Syntax
closure_expression = ("||" | "|" [ closure_param { "," closure_param } ] "|")
expression .
closure_param = identifier ":" type_expression .
type_expression = identifier [ "(" type_expression { "," type_expression } ")" ]
| "[" type_expression "]"
| "(" [ type_expression [ "," [ type_expression { "," type_expression } ] ] ] ")" .
Worked examples
|x: i32| x + 1
|pair: (i32, i32)| pair.0 + pair.1
|| 1..=5
(|x: f64| x)(1.0)
(|pair: (i32, i32)| pair.0 + pair.1)((2, 3))
(|rows: [[i32]]| rows)([[1], [2]])
|f: Fn((i32,), [i32])| f(42)
[]: [Fn((i32,), [i32])]
Exact rules
Closures:
|| bodydeclares a zero-parameter closure.|name: Type| bodydeclares one typed parameter.|a: T, b: U| bodydeclares multiple typed parameters in source order.- An annotation uses the host’s configured type environment: built-in
scalars and generic types, registered cloneable leaves, recursive tuple
types, recursive array types, and structural
Fnsignatures. Container shapes require no separate registration. |pair: (i32, i32)| pair.0 + pair.1declares one tuple-typed parameter, not two separate parameters.- The closure body is one expression.
- A closure body resolves its own parameters and does not capture free variables from an enclosing expression. Installed library operations remain available inside it.
- Parenthesize a closure to invoke it immediately. CEL checks the exact parameter and result shapes, including nested tuples and array element types. Arguments evaluate once, left to right; body errors propagate to the enclosing evaluation.
- Known signatures travel through nested calls and tuple intermediates. Conditional closure alternatives must have identical known signatures. An opaque host-provided closure with no known signature reports an unsupported-call error rather than guessing a result type. A host may still supply its own call operation, which takes precedence.
Host registration
CELParser::with_type_resolver supplies annotation lookup. A registered
leaf uses ValueType::cloneable_leaf::<T>() so parameters can clone
owned values without requiring Copy, equality, or debug formatting.
Adam supplies these descriptors through its existing TypeRegistry;
no second closure-specific type registry is needed. Results move from
the body into the caller and need no clone adapter of their own.
The Rust DynClosure::new, call, and call_boxed APIs remain available
for existing host integrations. DynClosure::typed and
DynSegment::push_closure additionally retain the complete metadata
needed by general CEL invocation.
Typed callable interoperability
Fn((A, B), R) has two parameters, Fn((A,), R) has one, and Fn((), R)
has none. The parameter list is always a tuple type: Fn(A, R) does not
stand for one parameter. A parameter can itself be a tuple, array, or callable.
Arrays compare whole signatures when checking element homogeneity.
Callable<Args, R> uses concrete canonical list arguments and an owned result.
FnOf<ArgsWitness, ResultWitness> supplies its complete structural witness.
Callable::from_fn binds a native function pointer;
Callable::from_prepared binds a matching PreparedSegment. Both expose the
same call(&args) interface with no signature comparisons during invocation.
Clones share execution state; recursive entry into the same carrier returns an
error. Body errors propagate. This interface prepares for a future compiled
backend; that backend is not implemented.
This example checks a typed empty array and calls native and interpreted functions through the same Rust interface. Callables need no equality capability to be array elements:
#![allow(unused)]
fn main() {
/// Preserves complete callable signatures even in empty arrays, without equality.
#[test]
fn typed_callable_arrays() {
use cel_parser::{CELParser, OpLookup};
use cel_runtime::{
Array, ArrayOf, CNil, CStackList, Callable, Cons, FnOf, IntoCStackList, Leaf, Nil,
};
type Args = Cons<Leaf<i32>, Nil>;
type Function = FnOf<Args, ArrayOf<Leaf<i32>>>;
let mut parser = CELParser::new(OpLookup::new());
let empty = parser.parse_str("[]: [Fn((i32,), [i32])]").unwrap();
let mut empty = empty.prepare::<Nil, ArrayOf<Function>>().unwrap();
assert!(empty.call(&().into_c_stack_list()).unwrap().is_empty());
let body = parser.parse_str("[|x: i32| [x]]").unwrap();
let mut body = body.prepare::<Nil, ArrayOf<Function>>().unwrap();
let interpreted = body.call(&().into_c_stack_list()).unwrap();
let native =
Callable::from_fn(|args: &CStackList<i32, CNil<()>>| Ok(Array::from_vec(vec![args.1])));
for function in [native, interpreted.into_vec().pop().unwrap()] {
assert_eq!(
function
.call(&(42,).into_c_stack_list())
.unwrap()
.as_slice(),
[42]
);
}
}
}
Edge cases
- Closure parameter lists do not accept a trailing comma.
(i32,)is a one-element tuple type;(i32)groups the scalar type.- Closure bodies do not introduce statement blocks or local binding syntax.
Standard library
These numeric operations are opt-in: the evaluation environment must install
cel-std with cel_std::install on its operation lookup. Without that
installation, the operations in this chapter are unavailable; the core
language still works unchanged. Installed operations use ordinary CEL calls.
The descriptive signatures below use type-family shorthand, not CEL generics:
| Shorthand | Supported concrete types |
|---|---|
T | i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64 |
S | i8, i16, i32, i64, i128, isize, f32, f64 |
F | f32, f64 |
Each occurrence of a shorthand in one call denotes the same concrete type.
There is no implicit numeric conversion: for example, min(1i32, 2i64) has no
matching overload. Unsupported types and arities fail during expression
compilation, rather than producing the evaluation errors documented below.
Return types always match the operand type; even rounded results remain floats.
Unsuffixed floating-point literals in the examples have type f64.
NaN and infinity below describe floating-point values, not CEL literal
spellings. Operations that produce NaN do not report an evaluation error
unless an entry explicitly says otherwise. NaN payloads and signs are not
promised.
min
min(a: T, b: T) -> T
Selects the smaller of two same-type numeric values.
Parameters
| Parameter | Description |
|---|---|
a, b | Values to compare, both of the same type T. |
Return value
The smaller operand. For floats, exactly one NaN operand causes the other
operand to be returned; two NaNs produce NaN. Infinities compare in the usual
order. Equal operands may yield either operand, so comparing -0.0 and +0.0
does not guarantee a particular zero sign.
Complexity
O(1) time and space.
Example
min(3i32, -5i32)
Returns -5i32.
See also
max
max(a: T, b: T) -> T
Selects the larger of two same-type numeric values.
Parameters
| Parameter | Description |
|---|---|
a, b | Values to compare, both of the same type T. |
Return value
The larger operand. For floats, exactly one NaN operand causes the other
operand to be returned; two NaNs produce NaN. Infinities compare in the usual
order. Equal operands may yield either operand, so comparing -0.0 and +0.0
does not guarantee a particular zero sign.
Complexity
O(1) time and space.
Example
max(3.5f64, 2.5f64)
Returns 3.5f64.
See also
clamp
clamp(x: T, lo: T, hi: T) -> T
Restricts a numeric value to an inclusive interval.
Parameters
| Parameter | Description |
|---|---|
x | Value to restrict. |
lo | Inclusive lower bound, of the same type as x. |
hi | Inclusive upper bound, of the same type as x. |
Return value
lo if x < lo, hi if x > hi, otherwise x, all in the same type T.
Equal bounds are allowed. With valid floating-point bounds, a NaN x produces
NaN; infinite values and bounds follow the same comparisons. Because signed
zeros compare equal, an in-range zero retains the sign of x.
Errors
Reports invalid clamp bounds if lo <= hi is false: either lo > hi, or
either floating-point bound is NaN. Bounds are checked even when x is NaN.
Complexity
O(1) time and space.
Example
clamp(-2i32, 0i32, 10i32)
Returns 0i32.
See also
abs
abs(x: S) -> S
Computes an absolute value for a signed integer or float. Unsigned integers are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Signed integer or floating-point value of type S. |
Return value
The magnitude of x, in the same type. Floating-point -0.0 becomes +0.0,
either infinity becomes positive infinity, and NaN produces NaN.
Errors
Reports arithmetic overflow for the minimum value of any supported signed
integer type, because its positive magnitude is not representable.
Complexity
O(1) time and space.
Example
abs(-42i64)
Returns 42i64.
See also
signum
signum(x: S) -> S
Extracts a signed integer’s or float’s sign. Unsigned integers are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Signed integer or floating-point value of type S. |
Return value
For integers, returns -1, 0, or 1 according to whether x is negative,
zero, or positive, in the same integer type.
For floats, returns -1.0 for negative values (including negative infinity
and -0.0), 1.0 for positive values (including positive infinity and
+0.0), or NaN for NaN. The result has the same floating-point type as x.
In particular, floating-point zero does not produce zero.
Complexity
O(1) time and space.
Examples
signum(-7i32)
Returns -1i32.
signum(-3.5)
Returns -1.0 (f64).
See also
sqrt
sqrt(x: F) -> F
Computes a floating-point square root. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The nonnegative square root of a nonnegative value, in the same type.
Negative values, including negative infinity, produce NaN rather than an
evaluation error. NaN produces NaN; positive infinity remains positive
infinity. Signed zeros retain their signs, so sqrt(-0.0) returns -0.0.
Complexity
O(1) time and space.
Example
sqrt(9.0f32)
Returns 3.0f32.
See also
floor
floor(x: F) -> F
Rounds a float toward negative infinity. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The greatest integer less than or equal to x, represented in the same
floating-point type. Infinities are unchanged, NaN produces NaN, and signed
zeros retain their signs.
Complexity
O(1) time and space.
Example
floor(-3.2)
Returns -4.0 (f64).
See also
ceil
ceil(x: F) -> F
Rounds a float toward positive infinity. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The least integer greater than or equal to x, represented in the same
floating-point type. Infinities are unchanged, NaN produces NaN, and signed
zeros retain their signs. A value strictly between -1.0 and 0.0 rounds
to -0.0.
Complexity
O(1) time and space.
Example
ceil(-3.2)
Returns -3.0 (f64).
See also
floor, trunc, round_ties_even.
trunc
trunc(x: F) -> F
Rounds a float toward zero. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The integer part of x, represented in the same floating-point type.
Infinities are unchanged and NaN produces NaN. Signed zeros retain their
signs; a negative value strictly between -1.0 and 0.0 becomes -0.0.
Complexity
O(1) time and space.
Example
trunc(-3.2)
Returns -3.0 (f64).
See also
fract
fract(x: F) -> F
Extracts a float’s fractional part. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
x - trunc(x), in the same floating-point type. For finite nonintegral
values the fraction has the sign of x, not necessarily a positive sign.
Finite integral values, including either signed zero, produce +0.0.
Either infinity or NaN produces NaN rather than an evaluation error.
Complexity
O(1) time and space.
Example
fract(-3.75f32)
Returns -0.75f32.
See also
round_ties_even
round_ties_even(x: F) -> F
Rounds a float to the nearest integer, resolving exact halfway cases toward the even integer. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The nearest integer, represented in the same floating-point type.
Infinities are unchanged and NaN produces NaN. Signed zeros retain their
signs; a negative input that rounds to zero produces -0.0.
This operation is named round_ties_even; round is not provided.
Complexity
O(1) time and space.
Example
round_ties_even(2.5)
Returns 2.0 (f64), because 2 is the even neighbor.
See also
Euclid library
The optional cel-euclid library provides concrete two-dimensional geometry
types and operations. A host installs it once on a fresh OpLookup with
cel_euclid::install. The core language does not install geometry implicitly.
Install cel-std separately for scalar operations;
either installation order preserves both libraries’ overloads.
Conventions
Every coordinate, extent, angle component, and matrix coefficient has type
f64. The Rust types use Euclid’s unknown units; CEL does not expose generic
unit parameters. Operand types must match the listed signatures exactly:
write 1.0, 1f64, or 1 as f64 where an operation requires f64.
There are no implicit numeric casts or return-type-only overloads.
Signatures use Rust-style parameter and return-type notation to describe CEL
calls, not to declare functions. T denotes only the concrete alternatives
listed with a signature; it is not CEL generic syntax.
Every named operation with a first argument also accepts receiver syntax:
name(value, args...) and value.name(args...) are equivalent. A unary
operation additionally accepts value.name. Only identity_transform()
has no receiver. Operators use their ordinary CEL spelling.
Except for normalize, inverse, and
outer_transformed_rect, operations preserve
Euclid’s ordinary floating-point behavior. Overflow, division by zero,
NaNs, and infinities do not by themselves produce evaluation errors.
Record construction does not validate finiteness, order bounds, or require
nonnegative sizes.
Types
Each record requires every listed field exactly once, in any order. Fields must have the indicated types. Missing, repeated, unknown, or incorrectly typed fields cause a parse error. Record construction takes constant time and space.
Point2D
A position with fields x: f64 and y: f64.
Point2D { x: 1.0, y: 2.0 }
// => Point2D { y: 2.0, x: 1.0 }
Adding a vector produces a point; subtracting two points produces a vector.
Access coordinates with x and y.
Vector2D
A displacement with fields x: f64 and y: f64.
Vector2D { x: 3.0, y: 4.0 }
// => Vector2D { y: 4.0, x: 3.0 }
Vectors support arithmetic, dot, cross,
length, and normalize.
Size2D
Two extents with fields width: f64 and height: f64.
Construction permits zero and negative extents.
Size2D { width: 4.0, height: 6.0 }
// => Size2D { height: 6.0, width: 4.0 }
Access extents with width and height.
Rect
An axis-aligned rectangle with fields origin: Point2D and size: Size2D.
Its maximum coordinates are the origin coordinates plus the extents.
Construction does not reorder negative extents.
Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}
// => Rect { size: Size2D { width: 4.0, height: 6.0 }, origin: Point2D { x: 1.0, y: 2.0 } }
See origin, size, and contains.
Box2D
An axis-aligned box with fields min: Point2D and max: Point2D.
Construction stores the supplied endpoints without sorting them.
Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}
// => Box2D { max: Point2D { x: 5.0, y: 8.0 }, min: Point2D { x: 1.0, y: 2.0 } }
See min and max, size, and contains.
Angle
An angle with one field, radians: f64. Construction does not wrap the
value to a particular interval.
Angle { radians: 1.5 }
// => radians(1.5)
Use degrees or radians to construct an angle
from a scalar or extract its magnitude.
Transform2D
An affine transform with six f64 fields:
m11, m12, m21, m22, m31, and m32.
It maps a point (x, y) to:
x' = m11 * x + m21 * y + m31
y' = m12 * x + m22 * y + m32
Transform2D {
m11: 1.0, m12: 0.0,
m21: 0.0, m22: 1.0,
m31: 0.0, m32: 0.0
}
// => identity_transform()
Use then to compose transforms. Vectors use only the linear
coefficients and ignore translation.
Operators
Equality: ==, !=
a: T == b: T -> bool
a: T != b: T -> bool
T is any one of the seven geometry types. Compares corresponding stored
components using f64 equality; it does not compare geometric equivalence.
Parameters
| Parameter | Description |
|---|---|
a, b | Geometry values of the same type. |
Return value
== returns true if all corresponding components compare equal.
!= returns its logical negation. A NaN component compares unequal even
to itself; positive and negative zero compare equal. Angles differing by
a full turn do not compare equal merely because they describe the same direction.
Complexity
Constant time and space.
Example
Point2D { x: 1.0, y: 2.0 } == Point2D { y: 2.0, x: 1.0 }
// => true
See also
Addition: +
point: Point2D + offset: Vector2D -> Point2D
a: Vector2D + b: Vector2D -> Vector2D
a: Size2D + b: Size2D -> Size2D
Adds corresponding components.
Parameters
| Parameter | Description |
|---|---|
point, offset | A position and the displacement to add. |
a, b | Two vectors or two sizes. |
Return value
A translated point, component-wise vector sum, or component-wise size sum. Vector-plus-point and point-plus-point are not supported.
Complexity
Constant time and space.
Example
Point2D { x: 1.0, y: 2.0 } + Vector2D { x: 3.0, y: 4.0 }
// => Point2D { x: 4.0, y: 6.0 }
See also
Subtraction: -
a: Point2D - b: Point2D -> Vector2D
point: Point2D - offset: Vector2D -> Point2D
a: Vector2D - b: Vector2D -> Vector2D
a: Size2D - b: Size2D -> Size2D
Subtracts corresponding components.
Parameters
| Parameter | Description |
|---|---|
a, b | Two points, two vectors, or two sizes. |
point, offset | A position and the displacement to subtract. |
Return value
For two points, the displacement from b to a. Otherwise, a point,
vector, or size containing the component-wise differences.
Complexity
Constant time and space.
Example
Point2D { x: 4.0, y: 6.0 } - Point2D { x: 1.0, y: 2.0 }
// => Vector2D { x: 3.0, y: 4.0 }
See also
+, distance_to.
Negation: unary -
-v: Vector2D -> Vector2D
Negates both components of a vector.
Parameters
| Parameter | Description |
|---|---|
v | The vector to negate. |
Return value
A vector with components -v.x and -v.y.
Complexity
Constant time and space.
Example
-Vector2D { x: 3.0, y: -4.0 }
// => Vector2D { x: -3.0, y: 4.0 }
See also
-.
Scaling: *, /
value: T * factor: f64 -> T
value: T / factor: f64 -> T
T is Vector2D or Size2D. Multiplies or divides each component by a scalar.
Parameters
| Parameter | Description |
|---|---|
value | A vector or size. |
factor | The scalar multiplier or divisor. |
Return value
A vector or size containing the scaled components. Scalar-left products
and point scaling are not supported. Division by zero follows f64
arithmetic rather than producing an evaluation error.
Complexity
Constant time and space.
Example
Vector2D { x: 3.0, y: 4.0 } * 2.0
// => Vector2D { x: 6.0, y: 8.0 }
Size2D { width: 4.0, height: 6.0 } / 2.0
// => Size2D { width: 2.0, height: 3.0 }
See also
Component and bounds access
is_finite
is_finite(value: T) -> bool
T is any one of the seven geometry types. Tests whether all stored scalar
components are finite.
Parameters
| Parameter | Description |
|---|---|
value | The geometry value to inspect. |
Return value
true if no stored component is NaN or infinite; otherwise false.
For Rect, this checks the origin and size, not the computed maximum
coordinates. It does not validate sizes, bounds ordering, or invertibility.
Complexity
Constant time and space.
Example
(Vector2D { x: 1.0, y: 2.0 } / 0.0).is_finite
// => false
See also
x, y
x(value: T) -> f64
y(value: T) -> f64
T is Point2D or Vector2D. Reads one coordinate or component.
Parameters
| Parameter | Description |
|---|---|
value | A point or vector. |
Return value
The stored x or y component, respectively.
Complexity
Constant time and space.
Example
(Point2D { x: 1.0, y: 2.0 }).y
// => 2.0
See also
width, height
width(value: T) -> f64
height(value: T) -> f64
T is Size2D, Rect, or Box2D. Reads one extent.
Parameters
| Parameter | Description |
|---|---|
value | A size, rectangle, or box. |
Return value
For sizes and rectangles, the corresponding stored size component.
For boxes, max.x - min.x or max.y - min.y. Negative extents are
returned unchanged rather than clamped to zero.
Complexity
Constant time and space.
Example
(Size2D { width: 4.0, height: 6.0 }).width
// => 4.0
See also
size, min_x, min_y, max_x, and max_y.
origin
origin(rect: Rect) -> Point2D
Reads a rectangle’s stored origin.
Parameters
| Parameter | Description |
|---|---|
rect | The rectangle to inspect. |
Return value
The origin field, without normalizing the rectangle.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).origin
// => Point2D { x: 1.0, y: 2.0 }
See also
size, min_x, min_y, max_x, and max_y.
size
size(value: Rect) -> Size2D
size(value: Box2D) -> Size2D
Returns both extents of a rectangle or box.
Parameters
| Parameter | Description |
|---|---|
value | A rectangle or box. |
Return value
For a rectangle, its stored size. For a box, the width and height
computed by subtracting min from max.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).size
// => Size2D { width: 4.0, height: 6.0 }
See also
min, max
min(bounds: Box2D) -> Point2D
max(bounds: Box2D) -> Point2D
Reads a box’s stored minimum or maximum point.
Parameters
| Parameter | Description |
|---|---|
bounds | The box to inspect. |
Return value
The stored min or max field. These overloads do not compute a
component-wise minimum or maximum of two geometry values.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).max
// => Point2D { x: 5.0, y: 8.0 }
See also
min_x, min_y, max_x, and max_y,
scalar min, scalar max.
min_x, min_y, max_x, max_y
min_x(value: T) -> f64
min_y(value: T) -> f64
max_x(value: T) -> f64
max_y(value: T) -> f64
T is Rect or Box2D. Reads one bound coordinate.
Parameters
| Parameter | Description |
|---|---|
value | A rectangle or box. |
Return value
For rectangles, minimum coordinates are the origin components; maximum
coordinates add the corresponding size components. For boxes, these
operations read the components of the stored min or max point.
They do not reorder inverted bounds.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).max_y
// => 8.0
See also
min and max, width and height.
center
center(value: T) -> Point2D
T is Rect or Box2D. Computes the center of the bounds.
Parameters
| Parameter | Description |
|---|---|
value | A rectangle or box. |
Return value
The midpoint computed using Euclid’s floating-point arithmetic, without validating or normalizing the bounds.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).center
// => Point2D { x: 3.0, y: 5.0 }
See also
Vector queries
dot
dot(a: Vector2D, b: Vector2D) -> f64
Computes the dot product of two vectors.
Parameters
| Parameter | Description |
|---|---|
a, b | The vectors to multiply. |
Return value
a.x * b.x + a.y * b.y, subject to floating-point rounding.
Complexity
Constant time and space.
Example
dot(Vector2D { x: 3.0, y: 4.0 }, Vector2D { x: 1.0, y: 2.0 })
// => 11.0
See also
cross
cross(a: Vector2D, b: Vector2D) -> f64
Computes the scalar two-dimensional cross product.
Parameters
| Parameter | Description |
|---|---|
a, b | The vectors, in operand order. |
Return value
a.x * b.y - a.y * b.x, subject to floating-point rounding.
Complexity
Constant time and space.
Example
cross(Vector2D { x: 3.0, y: 4.0 }, Vector2D { x: 1.0, y: 2.0 })
// => 2.0
See also
dot.
square_length
square_length(v: Vector2D) -> f64
Computes the squared length of a vector.
Parameters
| Parameter | Description |
|---|---|
v | The vector to measure. |
Return value
v.x * v.x + v.y * v.y, subject to floating-point rounding.
Intermediate products can overflow or underflow.
Complexity
Constant time and space.
Example
(Vector2D { x: 3.0, y: 4.0 }).square_length
// => 25.0
See also
length
length(v: Vector2D) -> f64
Computes a vector’s Euclidean length.
Parameters
| Parameter | Description |
|---|---|
v | The vector to measure. |
Return value
The square root of the squared length, subject to floating-point rounding, overflow, and underflow. This operation does not reject non-finite results.
Complexity
Constant time and space.
Example
(Vector2D { x: 3.0, y: 4.0 }).length
// => 5.0
See also
square_length, normalize, distance_to.
normalize
normalize(v: Vector2D) -> Vector2D
Returns a vector in the direction of v with unit length, subject to
floating-point rounding.
Parameters
| Parameter | Description |
|---|---|
v | The vector to normalize. |
Return value
A new vector obtained by dividing each component of v by its computed length.
Errors
Evaluation fails if the computed length is zero or non-finite, or if either result component is non-finite. Finite, nonzero components do not guarantee success: computing the length can underflow to zero or overflow to infinity.
Complexity
Constant time and space.
Example
normalize(Vector2D { x: 3.0, y: 4.0 })
// => Vector2D { x: 0.6, y: 0.8 }
Equivalent receiver-call syntax:
(Vector2D { x: 3.0, y: 4.0 }).normalize()
// => Vector2D { x: 0.6, y: 0.8 }
A zero vector produces an evaluation error:
normalize(Vector2D { x: 0.0, y: 0.0 })
// Evaluation error: normalize requires a positive finite length
See also
length, square_length, is_finite.
distance_to
distance_to(a: Point2D, b: Point2D) -> f64
Computes the Euclidean distance between two points.
Parameters
| Parameter | Description |
|---|---|
a, b | The points to measure between. |
Return value
The length of their displacement, subject to floating-point rounding, overflow, and underflow.
Complexity
Constant time and space.
Example
(Point2D { x: 1.0, y: 2.0 }).distance_to(Point2D { x: 4.0, y: 6.0 })
// => 5.0
See also
Bounds operations
translate
translate(value: T, offset: Vector2D) -> T
T is Rect or Box2D. Moves bounds by a displacement.
Parameters
| Parameter | Description |
|---|---|
value | The rectangle or box to move. |
offset | The displacement to add. |
Return value
For a rectangle, a translated origin with the original size. For a box, translated minimum and maximum points.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).translate(Vector2D { x: 3.0, y: 4.0 })
// => Rect { origin: Point2D { x: 4.0, y: 6.0 }, size: Size2D { width: 4.0, height: 6.0 } }
See also
+, translation.
inflate
inflate(value: T, dx: f64, dy: f64) -> T
T is Rect or Box2D. Expands bounds on both sides of each axis.
Parameters
| Parameter | Description |
|---|---|
value | The rectangle or box to expand. |
dx, dy | Expansion on each horizontal and vertical side. Negative values shrink. |
Return value
Bounds with minimum coordinates decreased by dx, dy and maximum
coordinates increased by dx, dy. The width changes by 2 * dx and
the height by 2 * dy. Excessive shrinking can produce negative extents;
it is not an evaluation error.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).inflate(1.0, 2.0)
// => Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 6.0, y: 10.0 } }
See also
to_box2d
to_box2d(rect: Rect) -> Box2D
Converts an origin-and-size rectangle to endpoint bounds.
Parameters
| Parameter | Description |
|---|---|
rect | The rectangle to convert. |
Return value
A box whose min is the rectangle’s origin and whose max is the origin
plus the size. The conversion does not normalize negative extents.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).to_box2d
// => Box2D { min: Point2D { x: 1.0, y: 2.0 }, max: Point2D { x: 5.0, y: 8.0 } }
See also
to_rect
to_rect(bounds: Box2D) -> Rect
Converts endpoint bounds to an origin-and-size rectangle.
Parameters
| Parameter | Description |
|---|---|
bounds | The box to convert. |
Return value
A rectangle whose origin is min and whose size is max - min.
Floating-point rounding means conversions need not round-trip exactly.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).to_rect
// => Rect { origin: Point2D { x: 1.0, y: 2.0 }, size: Size2D { width: 4.0, height: 6.0 } }
See also
contains
contains(bounds: Rect, point: Point2D) -> bool
contains(bounds: Box2D, point: Point2D) -> bool
Tests point containment using half-open bounds.
Parameters
| Parameter | Description |
|---|---|
bounds | The rectangle or box to test. |
point | The point to locate. |
Return value
true when both minimum coordinates are included and both maximum
coordinates are excluded:
min_x <= point.x < max_x and min_y <= point.y < max_y.
Empty or inverted bounds contain no points.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).contains(Point2D { x: 5.0, y: 4.0 })
// => false
See also
contains_rect, contains_box, intersects.
contains_rect
contains_rect(outer: Rect, inner: Rect) -> bool
Tests whether one rectangle contains another using Euclid’s bounds rules.
Parameters
| Parameter | Description |
|---|---|
outer | The containing rectangle. |
inner | The rectangle to test. |
Return value
true if inner is empty, or if outer is nonempty and contains all of
inner’s bounds. Coincident maximum edges are allowed for nonempty rectangles,
unlike point containment. NaN extents follow Euclid’s empty-bounds rules.
Complexity
Constant time and space.
Example
contains_rect(
Rect { origin: Point2D { x: 0.0, y: 0.0 }, size: Size2D { width: 10.0, height: 10.0 } },
Rect { origin: Point2D { x: 1.0, y: 2.0 }, size: Size2D { width: 4.0, height: 6.0 } }
)
// => true
See also
contains_box
contains_box(outer: Box2D, inner: Box2D) -> bool
Tests whether one box contains another using Euclid’s bounds rules.
Parameters
| Parameter | Description |
|---|---|
outer | The containing box. |
inner | The box to test. |
Return value
true if inner is empty, or if outer is nonempty and contains all of
inner’s bounds. Coincident maximum edges are allowed for nonempty boxes.
NaN endpoints follow Euclid’s empty-bounds rules.
Complexity
Constant time and space.
Example
contains_box(
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 10.0, y: 10.0 } },
Box2D { min: Point2D { x: 1.0, y: 2.0 }, max: Point2D { x: 5.0, y: 8.0 } }
)
// => true
See also
intersects
intersects(a: T, b: T) -> bool
T is Rect or Box2D. Tests whether two bounds overlap.
Parameters
| Parameter | Description |
|---|---|
a, b | Two rectangles or two boxes. |
Return value
true when both axes satisfy the strict endpoint comparisons
a.min < b.max and a.max > b.min. For nonempty finite bounds, this
means positive-area overlap; touching at an edge or corner returns false.
The operation does not separately reject empty or inverted bounds: an
empty box strictly inside another box can return true.
Complexity
Constant time and space.
Example
intersects(
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 2.0, y: 2.0 } },
Box2D { min: Point2D { x: 2.0, y: 0.0 }, max: Point2D { x: 4.0, y: 2.0 } }
)
// => false
intersects(
Box2D { min: Point2D { x: 1.0, y: 1.0 }, max: Point2D { x: 1.0, y: 1.0 } },
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 2.0, y: 2.0 } }
)
// => true
See also
union
union(a: T, b: T) -> T
T is Rect or Box2D. Computes enclosing axis-aligned bounds.
Parameters
| Parameter | Description |
|---|---|
a, b | Two rectangles or two boxes. |
Return value
The enclosing bounds under Euclid’s union rules. For nonempty finite inputs, the minimum coordinates are the component-wise minima and the maximum coordinates are the component-wise maxima. If one operand is empty, its bounds are ignored; if both are empty, the first operand supplies the bounds. Rectangles convert through endpoint bounds, so their extents can incur rounding. This is not a shape-set union.
Complexity
Constant time and space.
Example
union(
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 2.0, y: 2.0 } },
Box2D { min: Point2D { x: 1.0, y: 1.0 }, max: Point2D { x: 4.0, y: 3.0 } }
)
// => Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 4.0, y: 3.0 } }
See also
intersects, outer_transformed_rect.
Angles
degrees
degrees(value: f64) -> Angle
degrees(angle: Angle) -> f64
Constructs an angle from degrees or expresses an angle in degrees.
Parameters
| Parameter | Description |
|---|---|
value | An angle magnitude in degrees. |
angle | An angle to convert to a scalar. |
Return value
For f64, an angle stored in radians. For Angle, its magnitude in degrees.
Neither overload wraps the angle to a particular interval.
Complexity
Constant time and space.
Example
degrees(0.0).radians
// => 0.0
See also
radians
radians(value: f64) -> Angle
radians(angle: Angle) -> f64
Constructs an angle from radians or reads its stored magnitude.
Parameters
| Parameter | Description |
|---|---|
value | An angle magnitude in radians. |
angle | An angle whose magnitude is requested. |
Return value
For f64, an angle with the given stored radians. For Angle, its stored
radians component. Neither overload wraps the angle.
Complexity
Constant time and space.
Example
radians(1.5).radians
// => 1.5
See also
Affine transforms
m11, m12, m21, m22, m31, m32
m11(transform: Transform2D) -> f64
m12(transform: Transform2D) -> f64
m21(transform: Transform2D) -> f64
m22(transform: Transform2D) -> f64
m31(transform: Transform2D) -> f64
m32(transform: Transform2D) -> f64
Reads a stored affine matrix coefficient.
Parameters
| Parameter | Description |
|---|---|
transform | The transform to inspect. |
Return value
The coefficient named by the operation. m31 and m32 are translation
components; the other four form the linear part.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 20.0 }).m31
// => 10.0
See also
identity_transform
identity_transform() -> Transform2D
Constructs the identity affine transform.
Return value
A transform with m11 = m22 = 1.0 and all other coefficients zero.
It leaves finite points and vectors unchanged.
Complexity
Constant time and space.
Example
identity_transform().transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 1.0, y: 2.0 }
See also
translation
translation(offset: Vector2D) -> Transform2D
Constructs a translation transform.
Parameters
| Parameter | Description |
|---|---|
offset | The displacement to apply to points. |
Return value
A transform with identity linear coefficients and translation components
offset.x, offset.y. Applying it to a vector ignores the translation.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 20.0 })
.transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 11.0, y: 22.0 }
See also
translate, then, transform_vector.
scale
scale(sx: f64, sy: f64) -> Transform2D
Constructs an axis-aligned scaling transform about the origin.
Parameters
| Parameter | Description |
|---|---|
sx, sy | Horizontal and vertical scale factors. |
Return value
A transform with m11 = sx, m22 = sy, and all other coefficients zero.
Negative factors reflect the corresponding axes; zero factors produce a
singular transform. Construction does not reject either case.
Complexity
Constant time and space.
Example
scale(2.0, 3.0).transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 2.0, y: 6.0 }
See also
rotation
rotation(angle: Angle) -> Transform2D
Constructs a rotation about the origin.
Parameters
| Parameter | Description |
|---|---|
angle | The rotation angle. |
Return value
A transform with linear coefficients cos(angle), sin(angle),
-sin(angle), cos(angle) and zero translation.
Positive angles rotate (1, 0) toward (0, 1); the visual direction
depends on whether the consuming coordinate system’s y-axis points up or down.
Complexity
Constant time and space.
Example
rotation(degrees(0.0)).transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 1.0, y: 2.0 }
See also
then
then(first: Transform2D, second: Transform2D) -> Transform2D
Composes transforms in application order: first, then second.
Parameters
| Parameter | Description |
|---|---|
first | The transform applied first. |
second | The transform applied second. |
Return value
The composed affine transform. Composition generally does not commute. Its result can differ from two separately evaluated mappings because floating-point operations round at different points.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 0.0 })
.then(scale(2.0, 2.0))
.transform_point(Point2D { x: 1.0, y: 0.0 })
// => Point2D { x: 22.0, y: 0.0 }
See also
translation, scale, transform_point.
determinant
determinant(transform: Transform2D) -> f64
Computes the determinant of the linear part of an affine transform.
Parameters
| Parameter | Description |
|---|---|
transform | The transform to inspect. |
Return value
m11 * m22 - m12 * m21, subject to floating-point rounding.
Translation does not affect the result. A zero determinant indicates a
singular linear part; a nonzero result alone does not guarantee that
checked inversion succeeds.
Complexity
Constant time and space.
Example
scale(2.0, 3.0).determinant
// => 6.0
See also
inverse
inverse(transform: Transform2D) -> Transform2D
Computes a checked affine inverse.
Parameters
| Parameter | Description |
|---|---|
transform | The transform to invert. |
Return value
Euclid’s inverse with finite coefficients and no nonzero exact cofactor lost to a zero inverse coefficient. Exact-zero cofactors and nonzero subnormal coefficients are allowed.
The check does not guarantee exact-real arithmetic, numerical stability, or an exact round trip. Other rounding and conditioning errors retain Euclid’s floating-point behavior.
Errors
Evaluation fails for:
- Non-finite input coefficients.
- A zero or non-finite computed determinant.
- Non-finite inverse coefficients.
- A zero inverse coefficient whose exact cofactor is nonzero, including loss within translation-cofactor arithmetic before determinant scaling.
Complexity
Constant time and space.
Example
scale(2.0, 4.0).inverse().transform_point(Point2D { x: 6.0, y: 8.0 })
// => Point2D { x: 3.0, y: 2.0 }
See also
transform_point
transform_point(transform: Transform2D, point: Point2D) -> Point2D
Applies an affine transform to a point, including translation.
Parameters
| Parameter | Description |
|---|---|
transform | The mapping to apply. |
point | The input position. |
Return value
The point given by the coordinate equations in Transform2D. Mapping is unchecked and can return non-finite coordinates.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 0.0 })
.transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 11.0, y: 2.0 }
See also
transform_vector, outer_transformed_rect.
transform_vector
transform_vector(transform: Transform2D, vector: Vector2D) -> Vector2D
Applies the linear part of an affine transform to a displacement.
Parameters
| Parameter | Description |
|---|---|
transform | The mapping to apply. |
vector | The input displacement. |
Return value
A vector with components m11 * x + m21 * y and m12 * x + m22 * y.
The translation coefficients m31, m32 do not contribute.
Mapping is unchecked and can return non-finite components.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 20.0 })
.transform_vector(Vector2D { x: 3.0, y: 4.0 })
// => Vector2D { x: 3.0, y: 4.0 }
See also
outer_transformed_rect
outer_transformed_rect(transform: Transform2D, rect: Rect) -> Rect
Computes checked axis-aligned bounds enclosing all four transformed corners.
Parameters
| Parameter | Description |
|---|---|
transform | The affine mapping, including rotation, shear, or reflection. |
rect | The source rectangle. |
Return value
A finite rectangle enclosing all four corners after Euclid maps them.
If reconstructing a maximum from an origin and extent rounds inward,
the extent advances by one representable f64 step to enclose that maximum.
Zero source extents and singular transforms are allowed.
An outer bound is not an interior-containment test: its corners need not lie inside the transformed shape.
Errors
Evaluation fails for:
- Non-finite matrix coefficients, source origins, extents, or endpoints.
- Negative source extents, or positive extents whose computed endpoints do not advance beyond the corresponding origin.
- Projected edges that overflow or lose a nonzero extent completely to underflow, or non-finite projected spans.
- Non-finite mapped corners, or a positive projected span that collapses to zero after mapping.
- Output extents or enclosing endpoints that cannot be represented finitely.
Failure produces an evaluation error, not an empty rectangle.
Complexity
Constant time and space; maps four corners.
Example
scale(2.0, 3.0).outer_transformed_rect(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
})
// => Rect { origin: Point2D { x: 2.0, y: 6.0 }, size: Size2D { width: 8.0, height: 18.0 } }
See also
transform_point, union, inverse.
Host integration
This checked Rust example installs both libraries, constructs records with reversed field order, and composes transforms in application order:
#![allow(unused)]
fn main() {
/// Installing geometry adds exact records and receiver syntax, not implicit casts.
#[test]
fn optional_geometry() {
let mut lookup = cel_parser::OpLookup::new();
cel_std::install(&mut lookup);
cel_euclid::install(&mut lookup);
let mut parser = cel_parser::CELParser::new(lookup);
let mut segment = parser
.parse_str(
"translation(Vector2D { y: 0.0, x: 10.0 }).then(scale(2.0, 2.0))\
.transform_point(Point2D { y: 0.0, x: 1.0 }).x",
)
.unwrap();
assert_eq!(segment.call0::<f64>().unwrap(), 22.0);
let mut segment = parser
.parse_str("Point2D { y: 42.0, x: 0 as f64 }.y")
.unwrap();
assert_eq!(segment.call0::<f64>().unwrap(), 42.0);
}
}
Run cargo test -p cel-lang-book --test geometry to execute the host example
and the successful CEL examples marked with // => in this chapter.
Those comments describe results; they are not part of the signature notation.
Installing the library makes records and operations available. Geometry names
in closure annotations additionally require a configured TypeResolver.
The optional Adam adapter supplies type registration when the host enables
cel-euclid’s adam feature and calls cel_euclid::register_adam_types.
The host must still install operations on the lookup.
Geometry policy belongs in the consuming sheet
The library supplies general geometry, not crop-specific constraints. The Adam geometry chapter composes inverse bounds and scalar operations into a self-referencing fitting relationship. Its precision, coverage, and inward-margin requirements belong to that sheet, not to these operations.
Reference Manual
This chapter is the compact language reference for CEL expressions. It gathers the core grammar, token categories, type names, literal forms, operators, standard-library operations, collections, closures, and sharp edges in one place.
Concept
CEL is an expression language. Every source form in this chapter produces a value or helps compose one. Use the tutorial chapters for worked prose, and use this chapter when you need the exact grammar or a concise rule table.
Syntax
expression = range_expression .
range_expression = ".." [ or_expression ]
| "..=" or_expression
| or_expression [ ".." [ or_expression ] | "..=" or_expression ] .
or_expression = and_expression { "||" and_expression } .
and_expression = comparison_expression { "&&" comparison_expression } .
comparison_expression = bitwise_or_expression
[ ("==" | "!=" | "<" | ">" | "<=" | ">=") bitwise_or_expression ] .
bitwise_or_expression = bitwise_xor_expression { "|" bitwise_xor_expression } .
bitwise_xor_expression = bitwise_and_expression { "^" bitwise_and_expression } .
bitwise_and_expression = bitwise_shift_expression { "&" bitwise_shift_expression } .
bitwise_shift_expression = additive_expression { ("<<" | ">>") additive_expression } .
additive_expression = multiplicative_expression { ("+" | "-") multiplicative_expression } .
multiplicative_expression = cast_expression { ("*" | "/" | "%") cast_expression } .
cast_expression = unary_expression { "as" identifier } .
unary_expression = (("-" | "!") unary_expression) | postfix_expression .
postfix_expression = primary_expression {
"(" [ argument_list ] ")" | "." (unsuffixed_integer | identifier [ "(" [ argument_list ] ")" ])
} .
argument_list = expression { "," expression } .
primary_expression = literal
| identifier [ record_fields ]
| tuple_or_group
| array_expression
| if_expression
| closure_expression .
record_fields = "{"
[ field_initializer { "," field_initializer } [ "," ] ] "}" .
field_initializer = identifier ":" expression .
tuple_or_group = "(" [ expression [ "," [ expression { "," expression } ] ] ] ")" .
array_expression = "[" [ expression { "," expression } ] "]"
[ ":" type_expr ] .
type_expr = identifier [ "(" type_expr { "," type_expr } ")" ]
| "[" type_expr "]"
| "(" [ type_expr [ "," [ type_expr { "," type_expr } ] ] ] ")" .
if_expression = "if" expression "{" expression "}"
[ "else" ( "{" expression "}" | if_expression ) ] .
closure_expression = "||" expression
| "|" closure_param { "," closure_param } "|" expression .
closure_param = identifier ":" type_expr .
Worked examples
(1, 2).0
if ok && ready { [1, 2] } else { [3, 4] }
|pair: (i32, i32)| pair.0 + pair.1
1 + 2..=3 * 4
true as i32
Token table
| Category | Forms | Notes |
|---|---|---|
| Identifiers | name, value2, user_name | Bare names for values, callees, and closure parameters. |
| Reserved words | if, else, as, true, false | These spellings have fixed language roles. |
| Literal tokens | integers, floats, strings, characters, bytes, byte strings, C strings | Unit () comes from grammar, not from a standalone literal token. |
| Delimiters | ( ) [ ] { } | Used for grouping, tuples, calls, arrays, and if branches. |
| Structural punctuation | , : . | Used for separators, closure parameter types, and tuple indexing. |
| Operators | + - * / % ! && || == != < <= > >= & | ^ << >> .. ..= as | See the operator tables below. |
Type table
| Kind | Names or forms | Notes |
|---|---|---|
| Built-in scalar types | i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String | These are the named cast targets. Closure annotations also accept registered cloneable leaves and built-in generic types through the configured type resolver. |
| Other value forms | char, byte strings, C strings, unit, tuples, arrays, ranges, closures | Expressions can use these values, but they are not extra built-in scalar type names. |
| Structural types | (), (T,), (T, U), [T], Fn((A, B), R) | Compose recursively without registering each aggregate. (T) groups a type. Fn((), R) has no parameters; Fn((T,), R) has one. |
Literal table
| Form | Example | Result |
|---|---|---|
| Integer | 42, 42u16 | Unsuffixed integers default to i32. Integer suffixes are i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, and usize. |
| Floating-point | 3.5, 6.02e23f32 | Unsuffixed floating-point literals default to f64. Floating-point suffixes are f32 and f64. |
| Boolean | true, false | Produces bool. |
| String | "hello" | Produces String. |
| Character | 'a' | Produces char. |
| Byte | b'A' | Produces u8. |
| Byte string | b"bytes" | Produces a byte-string value. |
| C string | c"header" | Produces a C-string value. |
| Unit | () | Produces the unit value. |
Operator precedence table
| Level (low to high) | Forms | Associativity |
|---|---|---|
| 1 | range forms | non-chaining |
| 2 | || | left |
| 3 | && | left |
| 4 | ==, !=, <, <=, >, >= | non-chaining |
| 5 | | | left |
| 6 | ^ | left |
| 7 | & | left |
| 8 | <<, >> | left |
| 9 | +, - | left |
| 10 | *, /, % | left |
| 11 | as | left |
| 12 | unary -, ! | prefix |
| 13 | calls, .N, .name, .name(args) | left |
Operator rules table
| Forms | Accepted operands | Notes |
|---|---|---|
+ | homogeneous numeric operands, or String + String | Unsigned integers wrap. Signed integers report arithmetic overflow on overflow. Strings concatenate. |
binary - | homogeneous numeric operands | Unsigned integers wrap. Signed integers report arithmetic overflow on overflow. |
unary - | signed integers and floating-point values | Signed integer overflow reports arithmetic overflow. |
* | homogeneous numeric operands | Unsigned integers wrap. Signed integers report arithmetic overflow on overflow. |
/, % | homogeneous numeric operands | Integer failures report division by zero. |
&, |, ^ | homogeneous integer operands | Integer-only bitwise operators. |
<<, >> | integer left operand and integer right operand | The right operand must fit u32, and the shift count must be in range. Failures report shift overflow. |
! | bool | Logical negation. |
&&, || | bool and bool | Short-circuit logical operators. |
==, != | homogeneous numeric operands, homogeneous bool, or homogeneous String | Produces bool. |
<, <=, >, >= | homogeneous numeric operands or homogeneous String | Produces bool. |
| range forms | homogeneous numeric endpoints, or no endpoints for .. | ..= requires a right endpoint. Endpoints are full expressions. |
as | expression plus built-in scalar target name | Integer targets accept integer, floating-point, and bool sources. f32/f64 targets accept integer and floating-point sources. bool and String targets accept only their same-type sources. |
call and .N | a valid callee and arguments, or a tuple plus an unsuffixed integer | .N applies to tuples only. |
Numeric call table
All functions in this table belong to the standard library and are available when the evaluation environment installs it. See Standard library for worked examples and fuller prose.
| Function | Accepted operands | Result | Notes |
|---|---|---|---|
min(a, b), max(a, b) | same-type numeric operands: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64 | same as operands | No coercion. Successful calls are infallible. |
clamp(x, lo, hi) | same-type numeric operands from the same 14-type set | same as operands | Requires ordered bounds with lo <= hi; otherwise reports invalid clamp bounds. |
abs(x) | i8, i16, i32, i64, i128, isize, f32, f64 | same as operand | Minimum signed integer reports arithmetic overflow. |
signum(x) | i8, i16, i32, i64, i128, isize, f32, f64 | same as operand | No unsigned support. Successful calls are infallible. |
sqrt(x) | f32, f64 | same as operand | Float-only. Inputs below zero yield NaN rather than an error. |
floor(x), ceil(x), trunc(x), fract(x), round_ties_even(x) | f32, f64 | same as operand | Float-only. Successful calls are infallible. |
- None of the documented functions are integer-only.
sqrt,floor,ceil,trunc,fract, andround_ties_evenare float-only.min,max, andclampsupport both integers and floats.absandsignumsupport signed integers and floats, but not unsigned integers.
Collection table
| Form | Syntax | Rules |
|---|---|---|
| Unit | () | The empty parenthesized form is the unit value. |
| Grouping | (expr) | Changes grouping without creating a tuple. |
| Tuple | (expr,), (a, b, ...) | Positional value form. A 1-tuple requires the trailing comma. |
| Array | [a, b, ...] | Unannotated arrays are non-empty, homogeneous, and comma-separated. |
| Typed array | [a, b]: [T], []: [T] | The annotation states the complete array type and permits empty arrays. |
| Nested array | [[0], [1]], []: [[i32]] | Each element must still have the same array element type. |
| Tuple array | [(0, true)], []: [(i32, bool)] | Each positional type participates in homogeneity. |
| Callable array | []: [Fn((i32,), [i32])] | Empty arrays retain full signatures; equality is not required for storage. |
| Range | a..b, a..=b, a.., ..b, ..=b, .. | Endpoint-bearing forms require homogeneous numeric endpoints. |
Closure table
| Form | Meaning | Notes |
|---|---|---|
|| expr | zero-parameter closure | The body is one expression. |
|x: T| expr | one typed parameter | T resolves through the configured type environment and may recursively contain tuples or arrays. |
|x: T, y: U| expr | multiple typed parameters | Parameters are comma-separated and ordered left to right. |
|pair: (i32, i32)| expr | one tuple-typed parameter | Tuple parameters are still one parameter and use .N inside the body. |
| nested tuple type | |value: (i32, (f64, bool))| expr | Tuple types may nest recursively. |
(|x: f64| x)(1.0) | immediate invocation | CEL checks exact parameter/result shapes; arguments evaluate once, left to right. Unknown callable signatures fail explicitly. Host call operations retain precedence. |
Exact rules
- Calls accept zero or more comma-separated arguments and do not accept a trailing comma.
- Tuple indexing uses
.Nwith an unsuffixed integer and may be chained. - Unannotated arrays are non-empty and homogeneous. A complete type annotation permits an empty array and must match the element type exactly.
- Tuples and arrays are distinct forms and compose recursively, including registered leaves, source unit, and callable values.
- Rust interoperability uses
Array<T>, source-ordered canonical lists (Nil/Conswitnesses), andCallable<Args, R>(FnOfwitness).Array<Array<T>>converts toVec<Array<T>>, not allocation-preserving deep native vectors. See collections. - Arbitrary erased external values require complete shape checks.
PreparedSegmentandprepared_segment::PreparedRawSegmentvalidate at preparation, not each invocation. Clone, equality, and formatting are explicit independent capabilities. - Native Rust operations can serve interpreted CEL through typed adapters. A macro-compiled CEL backend does not yet exist.
- Comparison expressions consume one comparison operator.
- Range forms do not chain.
expr.nameandexpr.name()invoke the same host-provided unary operation.expr.name(a, b)passes the receiver first, followed by explicit arguments. Each operand evaluates once in source order. A value binding namednamedoes not hide a dot operation, and the member label is not a variable reference.(expr.name)()calls the operation’s result normally; it fails if the result is not callable. Dot operations, ordinary calls, and tuple indexing share postfix precedence and may be chained.TypeName { field: expression, ... }constructs a host-registered record. Constructor names belong to the type namespace, separate from value names. Field labels are not variable references. Every schema field is required exactly once; unknown, duplicate, missing, and incorrectly typed fields are errors. Field expressions evaluate once in written order, independent of schema order. An empty schema permitsTypeName {}; a non-empty field list permits a trailing comma.- Field types match exactly: an
f64field accepts1.0, not thei32literal1. The static checker uses installed record schemas and operation signatures; known custom types retain their identities instead of unifying with unrelated types. Unknown constructors are errors. Unresolved inputs and calls without metadata remain best-effort, but all their child expressions are still checked. - Unparenthesized record constructors are forbidden throughout an
ifcondition. Parentheses, call arguments, tuple elements, and array elements start unrestricted expression positions: writeif (Sample { value: 3.0 }.value) > 0.0 { ... }. The restriction propagates through operators, ranges, and closure bodies. - Members are reads/operations, not assignment targets. CEL has no member assignment.
- CEL does not use bracket postfix indexing such as
expr[0]. - CEL does not define map literals, object literals, or a ternary operator.
- A closure body resolves its own parameters and does not capture free variables from the enclosing expression.
- A one-element tuple type requires
(T,);(T)is grouping.
Edge cases
- Signed
+, signed binary-, signed unary-, and signed*overflow reportarithmetic overflow. - Integer
/and%failures reportdivision by zero. - Negative, too-large, or non-
u32-fitting shift counts reportshift overflow. a < b < cis not valid; writea < b && b < c.1..2..3is not valid...=is not valid without a right endpoint.[]is not valid.[(0, 1), (true, 2)]is not valid because element shapes differ.