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 divides source characters into tokens. It uses Rust token trees and inherits many behaviors from the Rust lexical structure. 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
}

Adjacent punctuation characters combine into single tokens: &&, ||, ==, !=, <=, >=, <<, >>, .., and ..=.

a&&b
x<=y
1..=5

Comments and trivia

Comments may appear between tokens wherever whitespace is allowed.

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. This restriction applies to value references and closure parameters. Member labels, record field labels, and type names have separate namespaces; CEL does not apply the value-name restriction to them. Boolean tokens remain literals in every namespace.

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 defines the unit value (); it is not a 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

Character, byte-string, C-string, and unit values are also valid in expressions, but are not additional built-in scalar type names.

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. The numeric token must fit its declared type before negation. For example, -128i8 reports an out-of-range literal because 128i8 does not fit. Write -127i8 - 1i8 to express i8’s minimum with checked arithmetic. The same rule applies to every signed width and to literal patterns.

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. 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 {}; non-empty field lists reject trailing commas. 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 embedding example registers a 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.

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

expression = closure_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 { postfix_suffix } .
postfix_suffix = "(" [ argument_list ] ")" | "." (unsuffixed_integer | identifier) .
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.
  • A bare closure is a complete expression, not an operator operand. Group it before applying an operator or postfix suffix: (|x: i32| x + 1)(2).
  • + 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.
  • The valid range forms are a..b, a..=b, a.., ..b, ..=b, and ...
  • ..= always requires a right endpoint.
  • Range endpoints are or_expressions. Group a closure or nested range used as an endpoint; endpoint types must still satisfy the operator’s requirements.
  • 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.
  • 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.

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 if expressions to select a branch value. 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

  • CEL supports the branch forms if, if ... else, and if ... else if ... chains.
  • The condition reserves the following body brace. Parenthesize a record constructor in the condition, such as if (Point { x: 0.0 }).x == 0.0 { 1 } else { 2 }. Branch bodies 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 ().

Collections

CEL uses tuples for fixed positional values and arrays for homogeneous sequences. 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.

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.
  • 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

CEL values map recursively to concrete Rust values:

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.

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].

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)]);
    }
}
}

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.

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.

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 bare closure occupies a complete expression; an operator operand requires parentheses around the closure. 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

expression = closure_expression | range_expression .
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

  • || body declares a zero-parameter closure; spaced empty pipes | | are not this token.
  • |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 bare closure body extends to its enclosing expression boundary. For example, || 1 + 2 returns 3; write (|| 1)() to apply a zero-parameter closure.
  • Group a closure before using it as an operator operand, cast input, or postfix receiver. a + |x: i32| x is a syntax error; a + (|x: i32| x) is syntactically valid, with operator/type checking performed separately.
  • Call arguments, tuple and array elements, branch bodies, and closure bodies are complete expressions and accept bare closures, as in f(|x: i32| x + 1).
  • Parameter names are unique and cannot use reserved value names.
  • 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 TypeRegistry. 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 support host integrations. DynClosure::typed and DynSegment::push_closure 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 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 support both ordinary and receiver calls: sqrt(x) and x.sqrt() select the same operation. The receiver supplies the first argument, so atan2(y, x) is also y.atan2(x).

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. Scalar return types match the floating-point or numeric operand type; even rounded results remain floats. powi takes an i32 exponent, and sin_cos returns a tuple whose two elements have the input float type. 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. Float operations delegate to the corresponding Rust f32 or f64 method. Transcendental results have Rust’s unspecified precision, which can vary across platforms and Rust versions; compare approximate results with a tolerance. Each operation takes O(1) time and space.

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. Unlike round, this operation resolves halfway cases toward the even neighbor rather than away from zero.

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.

round

round(x: F) -> F

Rounds x to the nearest integer, resolving halfway cases away from zero.

Return value

The rounded value in the input float type. NaN produces NaN, infinities remain unchanged, and signed zeros retain their signs. Negative inputs that round to zero produce -0.0.

Example

round(2.5)

Returns 3.0 (f64). In contrast, round_ties_even(2.5) returns 2.0.

See also

round_ties_even, floor, ceil.

sin

sin(x: F) -> F

Computes the sine of the angle x in radians.

Return value

The sine in the input float type. Infinite or NaN input produces NaN; signed zeros retain their signs.

Example

(30.0).to_radians().sin()

Returns approximately 0.5 (f64).

See also

cos, sin_cos, to_radians.

cos

cos(x: F) -> F

Computes the cosine of the angle x in radians.

Return value

The cosine in the input float type. Infinite or NaN input produces NaN. Either signed zero produces 1.0.

Example

cos(0.0f32)

Returns 1.0f32.

See also

sin, sin_cos.

tan

tan(x: F) -> F

Computes the tangent of the angle x in radians.

Return value

The tangent in the input float type. Infinite or NaN input produces NaN; signed zeros retain their signs. Results near odd multiples of pi / 2 are sensitive to the represented input angle.

Example

tan(0.0)

Returns 0.0 (f64).

See also

atan, to_radians.

asin

asin(x: F) -> F

Computes the inverse sine of x.

Return value

An angle in radians between -pi / 2 and pi / 2, in the input float type. Inputs outside [-1, 1], including infinities, produce NaN. NaN produces NaN; signed zeros retain their signs.

Example

asin(0.5).to_degrees()

Returns approximately 30.0 (f64).

See also

sin, acos, atan.

acos

acos(x: F) -> F

Computes the inverse cosine of x.

Return value

An angle in radians between 0 and pi, in the input float type. Inputs outside [-1, 1], including infinities, produce NaN. NaN produces NaN.

Example

acos(0.5).to_degrees()

Returns approximately 60.0 (f64).

See also

cos, asin.

atan

atan(x: F) -> F

Computes the inverse tangent of x.

Return value

An angle in radians between -pi / 2 and pi / 2, in the input float type. Positive and negative infinity approach the corresponding signed endpoint. NaN produces NaN; signed zeros retain their signs.

Example

atan(1.0).to_degrees()

Returns approximately 45.0 (f64).

See also

tan, atan2.

atan2

atan2(y: F, x: F) -> F

Computes the angle of the vector (x, y), retaining its quadrant.

Parameters

ParameterDescription
yThe vertical coordinate, also the receiver in y.atan2(x).
xThe horizontal coordinate of the same float type.

Return value

An angle in radians between -pi and pi, in the operand float type. Signed zeros and infinities follow Rust’s atan2 semantics; a NaN coordinate produces NaN. For negative x, +0.0 and -0.0 values of y select the positive and negative sides of the branch cut respectively.

Example

atan2(1.0, -1.0).to_degrees()

Returns approximately 135.0 (f64).

See also

atan, hypot, to_degrees.

sin_cos

sin_cos(x: F) -> (F, F)

Computes the sine and cosine of the angle x in radians.

Return value

A CEL tuple ordered as (sin(x), cos(x)), with both elements in the input float type. The tuple supports .0 and .1, destructuring, and tuple-typed arguments. Infinite or NaN input produces two NaNs. At signed zero, the sine retains the zero’s sign and the cosine is 1.0.

Example

sin_cos((30.0).to_radians()).0 * 10.0

Returns approximately 5.0 (f64), the vertical coordinate of a point 10 units from the origin at 30 degrees.

See also

sin, cos, to_radians.

to_radians

to_radians(degrees: F) -> F

Converts an angle in degrees to radians without wrapping it.

Return value

The angle in radians in the input float type. Signed zeros retain their signs; infinities retain their signs, and NaN produces NaN.

Example

to_radians(180.0)

Returns approximately 3.141592653589793 (f64).

See also

to_degrees, sin.

to_degrees

to_degrees(radians: F) -> F

Converts an angle in radians to degrees without wrapping it.

Return value

The angle in degrees in the input float type. Signed zeros retain their signs; infinities retain their signs, and NaN produces NaN. A sufficiently large finite input can overflow to infinity.

Example

to_radians(180.0).to_degrees()

Returns approximately 180.0 (f64).

See also

to_radians, atan2, rem_euclid.

hypot

hypot(x: F, y: F) -> F

Computes the length of a vector with same-type coordinates x and y.

Return value

The nonnegative hypotenuse, mathematically sqrt(x * x + y * y), in the operand float type. An infinite operand produces positive infinity even if the other operand is NaN. Otherwise, a NaN operand produces NaN.

Example

hypot(3.0f32, 4.0f32)

Returns 5.0f32.

See also

sqrt, atan2.

cbrt

cbrt(x: F) -> F

Computes the real cube root of x, including negative inputs.

Return value

The cube root in the input float type. Signed zeros and infinities retain their signs; NaN produces NaN.

Example

cbrt(-8.0)

Returns approximately -2.0 (f64).

See also

sqrt, powf.

powi

powi(x: F, exponent: i32) -> F

Raises x to an integer power. The exponent is always i32, including when x is f64; other exponent types have no matching overload.

Return value

The power in the base’s float type. Negative exponents compute reciprocal powers. Negative bases are supported. A zero exponent returns 1.0, including for a NaN base. Overflow and division by zero follow Rust’s floating-point behavior rather than reporting evaluation errors.

Example

powi(2.0f32, -3i32)

Returns 0.125f32.

See also

powf, recip.

powf

powf(x: F, exponent: F) -> F

Raises x to a same-type floating-point power.

Return value

The power in the operand float type. A finite negative base with a finite nonintegral exponent produces NaN. Other special values, overflow, and underflow follow Rust’s powf semantics without evaluation errors.

Example

powf(0.25, 0.5)

Returns approximately 0.5 (f64), a power curve suitable for adjusting normalized values.

See also

powi, exp, sqrt.

exp

exp(x: F) -> F

Computes e raised to x.

Return value

The natural exponential in the input float type. Negative infinity produces 0.0, positive infinity produces positive infinity, and NaN produces NaN. Finite inputs can underflow to zero or overflow to infinity.

Example

exp(1.0)

Returns approximately 2.718281828459045 (f64).

See also

ln, exp2, powf.

exp2

exp2(x: F) -> F

Computes 2 raised to x.

Return value

The base-two exponential in the input float type. Negative infinity produces 0.0, positive infinity produces positive infinity, and NaN produces NaN. Finite inputs can underflow to zero or overflow to infinity.

Example

exp2(3.0)

Returns 8.0 (f64).

See also

log2, exp.

ln

ln(x: F) -> F

Computes the natural logarithm of x.

Return value

The logarithm in the input float type. Either signed zero produces negative infinity; negative values produce NaN. Positive infinity remains positive infinity, and NaN produces NaN.

Example

ln(2.0)

Returns approximately 0.6931471805599453 (f64).

See also

exp, log, log2, log10.

log

log(x: F, base: F) -> F

Computes the logarithm of x in a same-type base.

Parameters

ParameterDescription
xThe value whose logarithm is computed.
baseThe logarithm base, normally positive and unequal to 1.0.

Return value

The logarithm in the operand float type, following Rust’s log semantics. Nonpositive values and invalid bases can produce NaN or infinity rather than evaluation errors; the operation does not validate the mathematical domain. Use the specialized logarithms for bases e, 2, or 10.

Example

log(8.0, 2.0)

Returns approximately 3.0 (f64).

See also

ln, log2, log10.

log2

log2(x: F) -> F

Computes the base-two logarithm of x.

Return value

The logarithm in the input float type. Either signed zero produces negative infinity; negative values produce NaN. Positive infinity remains positive infinity, and NaN produces NaN.

Example

log2(8.0)

Returns 3.0 (f64).

See also

exp2, ln, log.

log10

log10(x: F) -> F

Computes the base-ten logarithm of x.

Return value

The logarithm in the input float type. Either signed zero produces negative infinity; negative values produce NaN. Positive infinity remains positive infinity, and NaN produces NaN.

Example

log10(100.0)

Returns 2.0 (f64).

See also

powf, ln, log.

recip

recip(x: F) -> F

Computes the reciprocal 1 / x.

Return value

The reciprocal in the input float type. Signed zeros produce infinities of the same sign, infinities produce zeros of the same sign, and NaN produces NaN. These results are not evaluation errors.

Example

recip(4.0)

Returns 0.25 (f64).

See also

powi.

mul_add

mul_add(x: F, multiplier: F, addend: F) -> F

Computes x * multiplier + addend with one rounding step.

Return value

The fused multiply-add result in the operand float type. Unlike separate * and + operations, the intermediate product is not rounded. Special values follow Rust’s mul_add semantics without evaluation errors.

Example

mul_add(0.25, 8.0, 2.0)

Returns 4.0 (f64), interpolating one quarter of an 8-unit span starting at 2.0.

See also

clamp.

rem_euclid

rem_euclid(x: F, divisor: F) -> F

Computes the least nonnegative remainder, useful for wrapping angles or periodic coordinates. The divisor has the same float type as x.

Return value

The Euclidean remainder in the operand float type. The mathematical range is [0, abs(divisor)), but floating-point rounding can produce abs(divisor). A zero or NaN divisor, a NaN dividend, or an infinite dividend produces NaN rather than an evaluation error.

Example

rem_euclid(-450.0, 360.0)

Returns 270.0 (f64).

See also

to_degrees, fract.

copysign

copysign(magnitude: F, sign: F) -> F

Combines the magnitude of the first operand with the sign bit of the second.

Return value

The signed magnitude in the operand float type. The sign bit is copied even from negative zero or NaN; a NaN magnitude remains NaN.

Example

copysign(2.0, -1.0)

Returns -2.0 (f64).

See also

abs, signum.

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 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);
}
}

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.

Application constraints

The Adam geometry chapter composes inverse bounds and scalar operations into a self-referencing fitting relationship with application-specific precision, coverage, and inward-margin requirements.

Reference Manual

This chapter defines CEL’s grammar, tokens, types, literals, operators, standard-library operations, collections, and closures.

Concept

CEL is an expression language. Use the tutorial chapters for worked examples.

Syntax

CEL uses strict LL(1) parsing.

expression = closure_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
    [ comparison_operator 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 { postfix_suffix } .
postfix_suffix = "(" [ argument_list ] ")" | "." (unsuffixed_integer | identifier) .
comparison_operator = "==" | "!=" | "<" | ">" | "<=" | ">=" .
argument_list = expression { "," expression } .
primary_expression = literal
                   | value_identifier [ record_fields ]
                   | tuple_or_group
                   | array_expression
                   | if_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 .

These productions encode precedence and left associativity directly. Comparisons and ranges do not chain; logical operators may combine separate comparisons. A closure is a complete expression, not a primary operand. Its body consumes the remaining expression inside its delimiter boundary. Group a closure before applying an operator or postfix suffix to it. Arguments and collection elements are complete expressions and accept bare closures. A call suffix directly after a named member dispatches that member. Parenthesize the member before the call suffix to call its result instead.

value_identifier excludes if, else, as, true, and false.

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. Group closures and nested ranges used as endpoints.
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), round(x)f32, f64same as operandround resolves halfway cases away from zero; round_ties_even selects the even neighbor.
sin(x), cos(x), tan(x)f32, f64same as operandAngles in radians. Infinite inputs yield NaN.
asin(x), acos(x), atan(x)f32, f64same as operandResults in radians. asin and acos yield NaN outside [-1, 1].
atan2(y, x)same-type f32 or f64same as operandsQuadrant-aware angle in radians; vertical coordinate first.
sin_cos(x)f32, f64(f32, f32) or (f64, f64)CEL tuple ordered as sine then cosine; angle in radians.
to_radians(x), to_degrees(x)f32, f64same as operandAngle conversion without wrapping.
hypot(x, y)same-type f32 or f64same as operandsVector length.
cbrt(x)f32, f64same as operandReal cube root, including negative inputs.
powi(x, exponent)f32 or f64 base, i32 exponentsame as baseSupports negative integer exponents.
powf(x, exponent)same-type f32 or f64same as operandsFloating-point power.
exp(x), exp2(x)f32, f64same as operandPowers of e and 2.
ln(x), log2(x), log10(x)f32, f64same as operandLogarithms in bases e, 2, and 10; zeros yield negative infinity and negative inputs yield NaN.
log(x, base)same-type f32 or f64same as operandsArbitrary-base logarithm with Rust’s floating-point domain behavior.
recip(x)f32, f64same as operandReciprocal; signed zeros yield signed infinities.
mul_add(x, multiplier, addend)same-type f32 or f64same as operandsFused multiply-add with one rounding step.
rem_euclid(x, divisor)same-type f32 or f64same as operandsNonnegative remainder; rounding can produce abs(divisor). Zero divisor yields NaN.
copysign(magnitude, sign)same-type f32 or f64same as operandsCopies the second operand’s sign bit onto the first operand’s magnitude.

Both op(x, ...) and x.op(...) select these registered operations. Float calls retain Rust’s method semantics, including NaN and infinity; they do not introduce evaluation errors for mathematical domain violations.

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.
  • Comparison expressions consume one comparison operator.
  • Range forms do not chain.
  • Closures require parentheses as operator operands, including range endpoints, cast inputs, and postfix receivers. Complete-expression positions, including call arguments, tuple elements, array elements, and branch bodies, accept bare closures.
  • 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 {}. Field lists do not permit trailing commas.
  • 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.