Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Lexical conventions explains tokens, whitespace, comments, identifiers, and punctuation.
  2. Literals and types explains scalar, compound, and closure values.
  3. Operators explains precedence, associativity, arithmetic, comparison, logical, bitwise, cast, and range operators.
  4. Conditional expressions explains value-producing if expressions.
  5. Collections explains tuples and homogeneous arrays.
  6. Closures explains closure literals and parameter typing.
  7. 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:

  • if
  • else
  • as
  • true
  • false

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
  • { } for if branches

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.

CategoryTypes
Signed integersi8, i16, i32, i64, i128, isize
Unsigned integersu8, u16, u32, u64, u128, usize
Floating-point numbersf32, f64
Other scalarsbool, 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.

SuffixResulting type
nonei32
i8i8
i16i16
i32i32
i64i64
i128i128
isizeisize
u8u8
u16u16
u32u32
u64u64
u128u128
usizeusize

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.

SuffixResulting type
nonef64
f64f64
f32f32
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.
  • || expr and |x: T| expr are 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:

  1. range forms
  2. ||
  3. &&
  4. comparison operators
  5. bitwise |
  6. bitwise ^
  7. bitwise &
  8. shifts << and >>
  9. additive + and -
  10. multiplicative *, /, and %
  11. casts with as
  12. unary - and !
  13. 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.name and receiver.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 of receiver.name(). Named members, calls, and .N share 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 and String + String. Unsigned integer addition wraps. Signed integer addition reports arithmetic overflow on 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 report arithmetic overflow on overflow.
  • * accepts homogeneous numeric operands. Unsigned integer multiplication wraps. Signed integer multiplication reports arithmetic overflow on overflow.
  • / and % accept homogeneous numeric operands. Floating-point division and remainder use normal floating-point behavior. Integer failures report division by zero.
  • &, |, and ^ accept homogeneous integer operands only.
  • << and >> accept integer operands only. The right operand must fit in u32, and the shift count must be in range. Shift failures report shift overflow.
  • ! accepts bool only.
  • && and || accept bool operands only and short-circuit.
  • == and != accept homogeneous numeric operands, homogeneous bool, or homogeneous String.
  • <, <=, >, and >= accept homogeneous numeric operands or homogeneous String.
  • 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..3 is not valid.
  • 1 + 2..3 * 4 means (1 + 2)..(3 * 4).
  • as converts 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, and String.
  • Integer targets accept integer, floating-point, and bool sources. Floating-point targets accept integer and floating-point sources. bool and String targets 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 f32 checks finite range before narrowing.
  • true casts to integer 1, and false casts to integer 0.
  • 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, and String-to-number or String-to-bool casts are not valid.
  • Casts associate from left to right, so x as T as U applies the first cast before the second.
  • See Conditional expressions for if expressions, and the Reference Manual for a summary.

Edge cases

  • Write a < b && b < c, not a < 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 report shift overflow.
  • Signed +, signed binary -, signed unary -, and signed * overflows report arithmetic overflow.
  • Integer / and % report division by zero when 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

  • if is an expression, not a statement. It yields the value of the selected branch.
  • CEL supports the branch forms if, if ... else, and if ... else if ... chains.
  • The condition and every branch body are ordinary expressions.
  • Omitting the final else supplies an implicit () branch, so the remaining branches must still be type-compatible with unit.
  • The braces belong to if syntax. CEL does not use free-standing block expressions.

Edge cases

  • if flag { () } else if other { () } is valid without a final else because 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 .0 or .1. Named postfixes such as value.name invoke 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 shapeRust valueRepresentation witness
registered leaf TTLeaf<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 .0i32 is 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:

  • || body declares a zero-parameter closure.
  • |name: Type| body declares one typed parameter.
  • |a: T, b: U| body declares 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 Fn signatures. Container shapes require no separate registration.
  • |pair: (i32, i32)| pair.0 + pair.1 declares 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:

ShorthandSupported concrete types
Ti8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
Si8, i16, i32, i64, i128, isize, f32, f64
Ff32, 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

ParameterDescription
a, bValues 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, clamp.

max

max(a: T, b: T) -> T

Selects the larger of two same-type numeric values.

Parameters

ParameterDescription
a, bValues 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

min, clamp.

clamp

clamp(x: T, lo: T, hi: T) -> T

Restricts a numeric value to an inclusive interval.

Parameters

ParameterDescription
xValue to restrict.
loInclusive lower bound, of the same type as x.
hiInclusive 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

min, max.

abs

abs(x: S) -> S

Computes an absolute value for a signed integer or float. Unsigned integers are not supported.

Parameters

ParameterDescription
xSigned 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, clamp.

signum

signum(x: S) -> S

Extracts a signed integer’s or float’s sign. Unsigned integers are not supported.

Parameters

ParameterDescription
xSigned 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

abs, min, max.

sqrt

sqrt(x: F) -> F

Computes a floating-point square root. Integer operands are not supported.

Parameters

ParameterDescription
xFloating-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

abs, clamp.

floor

floor(x: F) -> F

Rounds a float toward negative infinity. Integer operands are not supported.

Parameters

ParameterDescription
xFloating-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, trunc, round_ties_even.

ceil

ceil(x: F) -> F

Rounds a float toward positive infinity. Integer operands are not supported.

Parameters

ParameterDescription
xFloating-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

ParameterDescription
xFloating-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, floor, ceil.

fract

fract(x: F) -> F

Extracts a float’s fractional part. Integer operands are not supported.

Parameters

ParameterDescription
xFloating-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

trunc, floor, ceil.

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

ParameterDescription
xFloating-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

floor, ceil, trunc.

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

ParameterDescription
a, bGeometry 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

is_finite.

Addition: +

point: Point2D + offset: Vector2D -> Point2D
a: Vector2D + b: Vector2D -> Vector2D
a: Size2D + b: Size2D -> Size2D

Adds corresponding components.

Parameters

ParameterDescription
point, offsetA position and the displacement to add.
a, bTwo 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

-, translate.

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

ParameterDescription
a, bTwo points, two vectors, or two sizes.
point, offsetA 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

ParameterDescription
vThe 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

ParameterDescription
valueA vector or size.
factorThe 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

scale, normalize.

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

ParameterDescription
valueThe 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

normalize, inverse.

x, y

x(value: T) -> f64
y(value: T) -> f64

T is Point2D or Vector2D. Reads one coordinate or component.

Parameters

ParameterDescription
valueA 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

origin, min and max.

width, height

width(value: T) -> f64
height(value: T) -> f64

T is Size2D, Rect, or Box2D. Reads one extent.

Parameters

ParameterDescription
valueA 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

ParameterDescription
rectThe 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

ParameterDescription
valueA 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

width and height, origin.

min, max

min(bounds: Box2D) -> Point2D
max(bounds: Box2D) -> Point2D

Reads a box’s stored minimum or maximum point.

Parameters

ParameterDescription
boundsThe 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

ParameterDescription
valueA 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

ParameterDescription
valueA 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

origin, min and max.

Vector queries

dot

dot(a: Vector2D, b: Vector2D) -> f64

Computes the dot product of two vectors.

Parameters

ParameterDescription
a, bThe 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, square_length.

cross

cross(a: Vector2D, b: Vector2D) -> f64

Computes the scalar two-dimensional cross product.

Parameters

ParameterDescription
a, bThe 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

ParameterDescription
vThe 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, dot, sqrt.

length

length(v: Vector2D) -> f64

Computes a vector’s Euclidean length.

Parameters

ParameterDescription
vThe 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

ParameterDescription
vThe 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

ParameterDescription
a, bThe 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

-, length.

Bounds operations

translate

translate(value: T, offset: Vector2D) -> T

T is Rect or Box2D. Moves bounds by a displacement.

Parameters

ParameterDescription
valueThe rectangle or box to move.
offsetThe 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

ParameterDescription
valueThe rectangle or box to expand.
dx, dyExpansion 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

translate, width and height.

to_box2d

to_box2d(rect: Rect) -> Box2D

Converts an origin-and-size rectangle to endpoint bounds.

Parameters

ParameterDescription
rectThe 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, min and max.

to_rect

to_rect(bounds: Box2D) -> Rect

Converts endpoint bounds to an origin-and-size rectangle.

Parameters

ParameterDescription
boundsThe 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

to_box2d, size.

contains

contains(bounds: Rect, point: Point2D) -> bool
contains(bounds: Box2D, point: Point2D) -> bool

Tests point containment using half-open bounds.

Parameters

ParameterDescription
boundsThe rectangle or box to test.
pointThe 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

ParameterDescription
outerThe containing rectangle.
innerThe 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, contains_box.

contains_box

contains_box(outer: Box2D, inner: Box2D) -> bool

Tests whether one box contains another using Euclid’s bounds rules.

Parameters

ParameterDescription
outerThe containing box.
innerThe 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

contains, contains_rect.

intersects

intersects(a: T, b: T) -> bool

T is Rect or Box2D. Tests whether two bounds overlap.

Parameters

ParameterDescription
a, bTwo 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

contains, union.

union

union(a: T, b: T) -> T

T is Rect or Box2D. Computes enclosing axis-aligned bounds.

Parameters

ParameterDescription
a, bTwo 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

ParameterDescription
valueAn angle magnitude in degrees.
angleAn 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, rotation.

radians

radians(value: f64) -> Angle
radians(angle: Angle) -> f64

Constructs an angle from radians or reads its stored magnitude.

Parameters

ParameterDescription
valueAn angle magnitude in radians.
angleAn 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

degrees, rotation.

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

ParameterDescription
transformThe 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

transform_point, determinant.

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, scale, rotation.

translation

translation(offset: Vector2D) -> Transform2D

Constructs a translation transform.

Parameters

ParameterDescription
offsetThe 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

ParameterDescription
sx, syHorizontal 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

* and /, then, inverse.

rotation

rotation(angle: Angle) -> Transform2D

Constructs a rotation about the origin.

Parameters

ParameterDescription
angleThe 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

degrees, radians, then.

then

then(first: Transform2D, second: Transform2D) -> Transform2D

Composes transforms in application order: first, then second.

Parameters

ParameterDescription
firstThe transform applied first.
secondThe 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

ParameterDescription
transformThe 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, is_finite.

inverse

inverse(transform: Transform2D) -> Transform2D

Computes a checked affine inverse.

Parameters

ParameterDescription
transformThe 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

determinant, then, is_finite.

transform_point

transform_point(transform: Transform2D, point: Point2D) -> Point2D

Applies an affine transform to a point, including translation.

Parameters

ParameterDescription
transformThe mapping to apply.
pointThe 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

ParameterDescription
transformThe mapping to apply.
vectorThe 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

transform_point, then.

outer_transformed_rect

outer_transformed_rect(transform: Transform2D, rect: Rect) -> Rect

Computes checked axis-aligned bounds enclosing all four transformed corners.

Parameters

ParameterDescription
transformThe affine mapping, including rotation, shear, or reflection.
rectThe 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

CategoryFormsNotes
Identifiersname, value2, user_nameBare names for values, callees, and closure parameters.
Reserved wordsif, else, as, true, falseThese spellings have fixed language roles.
Literal tokensintegers, floats, strings, characters, bytes, byte strings, C stringsUnit () 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+ - * / % ! && || == != < <= > >= & | ^ << >> .. ..= asSee the operator tables below.

Type table

KindNames or formsNotes
Built-in scalar typesi8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, StringThese are the named cast targets. Closure annotations also accept registered cloneable leaves and built-in generic types through the configured type resolver.
Other value formschar, byte strings, C strings, unit, tuples, arrays, ranges, closuresExpressions 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

FormExampleResult
Integer42, 42u16Unsuffixed integers default to i32. Integer suffixes are i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, and usize.
Floating-point3.5, 6.02e23f32Unsuffixed floating-point literals default to f64. Floating-point suffixes are f32 and f64.
Booleantrue, falseProduces bool.
String"hello"Produces String.
Character'a'Produces char.
Byteb'A'Produces u8.
Byte stringb"bytes"Produces a byte-string value.
C stringc"header"Produces a C-string value.
Unit()Produces the unit value.

Operator precedence table

Level (low to high)FormsAssociativity
1range formsnon-chaining
2||left
3&&left
4==, !=, <, <=, >, >=non-chaining
5|left
6^left
7&left
8<<, >>left
9+, -left
10*, /, %left
11asleft
12unary -, !prefix
13calls, .N, .name, .name(args)left

Operator rules table

FormsAccepted operandsNotes
+homogeneous numeric operands, or String + StringUnsigned integers wrap. Signed integers report arithmetic overflow on overflow. Strings concatenate.
binary -homogeneous numeric operandsUnsigned integers wrap. Signed integers report arithmetic overflow on overflow.
unary -signed integers and floating-point valuesSigned integer overflow reports arithmetic overflow.
*homogeneous numeric operandsUnsigned integers wrap. Signed integers report arithmetic overflow on overflow.
/, %homogeneous numeric operandsInteger failures report division by zero.
&, |, ^homogeneous integer operandsInteger-only bitwise operators.
<<, >>integer left operand and integer right operandThe right operand must fit u32, and the shift count must be in range. Failures report shift overflow.
!boolLogical negation.
&&, ||bool and boolShort-circuit logical operators.
==, !=homogeneous numeric operands, homogeneous bool, or homogeneous StringProduces bool.
<, <=, >, >=homogeneous numeric operands or homogeneous StringProduces bool.
range formshomogeneous numeric endpoints, or no endpoints for ....= requires a right endpoint. Endpoints are full expressions.
asexpression plus built-in scalar target nameInteger 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 .Na 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.

FunctionAccepted operandsResultNotes
min(a, b), max(a, b)same-type numeric operands: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64same as operandsNo coercion. Successful calls are infallible.
clamp(x, lo, hi)same-type numeric operands from the same 14-type setsame as operandsRequires ordered bounds with lo <= hi; otherwise reports invalid clamp bounds.
abs(x)i8, i16, i32, i64, i128, isize, f32, f64same as operandMinimum signed integer reports arithmetic overflow.
signum(x)i8, i16, i32, i64, i128, isize, f32, f64same as operandNo unsigned support. Successful calls are infallible.
sqrt(x)f32, f64same as operandFloat-only. Inputs below zero yield NaN rather than an error.
floor(x), ceil(x), trunc(x), fract(x), round_ties_even(x)f32, f64same as operandFloat-only. Successful calls are infallible.
  • None of the documented functions are integer-only.
  • sqrt, floor, ceil, trunc, fract, and round_ties_even are float-only.
  • min, max, and clamp support both integers and floats.
  • abs and signum support signed integers and floats, but not unsigned integers.

Collection table

FormSyntaxRules
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.
Rangea..b, a..=b, a.., ..b, ..=b, ..Endpoint-bearing forms require homogeneous numeric endpoints.

Closure table

FormMeaningNotes
|| exprzero-parameter closureThe body is one expression.
|x: T| exprone typed parameterT resolves through the configured type environment and may recursively contain tuples or arrays.
|x: T, y: U| exprmultiple typed parametersParameters are comma-separated and ordered left to right.
|pair: (i32, i32)| exprone tuple-typed parameterTuple parameters are still one parameter and use .N inside the body.
nested tuple type|value: (i32, (f64, bool))| exprTuple types may nest recursively.
(|x: f64| x)(1.0)immediate invocationCEL 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 .N with 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/Cons witnesses), and Callable<Args, R> (FnOf witness). Array<Array<T>> converts to Vec<Array<T>>, not allocation-preserving deep native vectors. See collections.
  • Arbitrary erased external values require complete shape checks. PreparedSegment and prepared_segment::PreparedRawSegment validate 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.name and expr.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 named name does 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 permits TypeName {}; a non-empty field list permits a trailing comma.
  • Field types match exactly: an f64 field accepts 1.0, not the i32 literal 1. 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 if condition. Parentheses, call arguments, tuple elements, and array elements start unrestricted expression positions: write if (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 report arithmetic overflow.
  • Integer / and % failures report division by zero.
  • Negative, too-large, or non-u32-fitting shift counts report shift overflow.
  • a < b < c is not valid; write a < b && b < c.
  • 1..2..3 is not valid.
  • ..= is not valid without a right endpoint.
  • [] is not valid.
  • [(0, 1), (true, 2)] is not valid because element shapes differ.