Introduction
This book is a tutorial and reference for CEL: its tokens, literals, basic types, operators, and collections.
Every CEL source form is an expression: it produces a value or helps compose one. Identifiers, calls, operators, collections, control flow, casts, and closures all participate in the same expression language.
The tutorial chapters follow this order:
- Lexical conventions explains tokens, whitespace, comments, identifiers, and punctuation.
- Literals and types explains scalar, compound, and closure values.
- Operators explains precedence, associativity, arithmetic, comparison, logical, bitwise, cast, and range operators.
- Conditional expressions explains
value-producing
ifexpressions. - Collections explains tuples and homogeneous arrays.
- Closures explains closure literals and parameter typing.
- Standard library explains additional operations available when the evaluation environment installs the library.
The Reference Manual gathers the complete grammar, exact operator rules, type names, library operations, and edge cases in one place.
Lexical conventions
The lexer 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:
ifelseastruefalse
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{}forifbranches
It uses these structural punctuation marks inside expressions:
,between tuple elements, array elements, and call arguments:in typed closure parameters.before an unsuffixed tuple index
(1, 2)
[0, 1, 2]
|x: i32| x + 1
point.0
Operators
Operator tokens carry arithmetic, comparison, logical, bitwise, cast, and range syntax.
+ - * / %
== != < <= > >=
! && ||
& | ^ << >>
as
.. ..=
The grammar determines where each operator may appear and how tightly it binds. See Reference Manual for the full expression grammar.
Literals and types
CEL source includes literal values for numbers, booleans, text, characters, and bytes. It also uses tuple, array, range, and closure syntax to build larger values from expressions. For the full grammar of these forms, see the Reference Manual.
Built-in scalar type names
These sixteen names are the built-in scalar type names used by as casts and
typed closure parameters.
| Category | Types |
|---|---|
| Signed integers | i8, i16, i32, i64, i128, isize |
| Unsigned integers | u8, u16, u32, u64, u128, usize |
| Floating-point numbers | f32, f64 |
| Other scalars | bool, String |
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.
| Suffix | Resulting type |
|---|---|
| none | i32 |
i8 | i8 |
i16 | i16 |
i32 | i32 |
i64 | i64 |
i128 | i128 |
isize | isize |
u8 | u8 |
u16 | u16 |
u32 | u32 |
u64 | u64 |
u128 | u128 |
usize | usize |
Unary - combines with a numeric literal to form a negative numeric
expression. 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.
| Suffix | Resulting type |
|---|---|
| none | f64 |
f64 | f64 |
f32 | f32 |
3.5
1e3
0.25f64
6.022e23f32
-3.5
Boolean, string, character, byte, and unit values
Boolean literals are true and false. String literals produce String
values. Character literals produce char values. Byte literals produce u8
values. Byte-string and C-string literals produce byte-oriented string values.
Write the unit value as ().
true
false
"hello"
'a'
b'A'
b"bytes"
c"header"
()
A byte literal such as b'A' and an explicitly suffixed integer such as 65u8
both denote u8 values.
Compound values
CEL also builds compound values from ordinary expressions:
()is the unit value,(expr)is grouping,(expr,)is a 1-tuple, and(a, b, c)is a tuple.- Arrays are homogeneous bracketed lists. An unannotated array is non-empty;
a type annotation such as
[]: [i32]supplies the element type for an empty array. || exprand|x: T| exprare closure values.- These forms can nest inside calls, tuples, arrays, conditionals, and one another wherever the grammar permits.
- Array homogeneity compares complete recursive shapes, including every tuple
field and callable parameter/result.
[]: [(i32, [f64])]and[]: [Fn((i32,), [i32])]need no registration of those aggregate shapes.
(1,)
(1, 2, 3)
[0, 1, 2]
[0, 1]: [i32]
[]: [i32]
|x: i32| x + 1
Source () and the empty tuple type are one canonical type, witnessed by Nil
and represented by CNil<()>. Bridge ordinary Rust () with the zero-element
IntoCStackList / IntoRustTuple conversion. 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:
- range forms
||&&- comparison operators
- bitwise
| - bitwise
^ - bitwise
& - shifts
<<and>> - additive
+and- - multiplicative
*,/, and% - casts with
as - unary
-and! - postfix calls, tuple indexing, and named members
Within one precedence level, binary operators associate to the left. Comparison expressions consume one comparison operator, and range forms do not chain.
- A call is a postfix
()form with zero or more comma-separated arguments. receiver.nameandreceiver.name()both invoke a unary named operation, ignoring same-named value bindings.receiver.name(a, b)supplies the receiver first, then explicit arguments; each evaluates once in source order. The member label is not a variable reference, even inside a closure.- Grouping changes call semantics:
(receiver.name)()calls the accessor’s result as a value. It is not another spelling ofreceiver.name(). Named members, calls, and.Nshare postfix precedence and chain left to right. Neither named members nor tuple indices support assignment. - Hosts supply member signatures, not a fixed set of receiver types. The checker rejects known incompatible operands and preserves registered result types. Calls without metadata remain best-effort and still check every argument.
- 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 andString + String. Unsigned integer addition wraps. Signed integer addition reportsarithmetic overflowon overflow.- Binary
-accepts homogeneous numeric operands. Unary-accepts only signed integers and floating-point values. Unsigned integer subtraction wraps. Signed binary subtraction and signed unary negation reportarithmetic overflowon overflow. *accepts homogeneous numeric operands. Unsigned integer multiplication wraps. Signed integer multiplication reportsarithmetic overflowon overflow./and%accept homogeneous numeric operands. Floating-point division and remainder use normal floating-point behavior. Integer failures reportdivision by zero.&,|, and^accept homogeneous integer operands only.<<and>>accept integer operands only. The right operand must fit inu32, and the shift count must be in range. Shift failures reportshift overflow.!acceptsboolonly.&&and||acceptbooloperands only and short-circuit.==and!=accept homogeneous numeric operands, homogeneousbool, or homogeneousString.<,<=,>, and>=accept homogeneous numeric operands or homogeneousString.- 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..3is not valid. 1 + 2..3 * 4means(1 + 2)..(3 * 4).asconverts a value to one of the sixteen built-in scalar type names:i8,i16,i32,i64,i128,isize,u8,u16,u32,u64,u128,usize,f32,f64,bool, andString.- Integer targets accept integer, floating-point, and
boolsources. Floating-point targets accept integer and floating-point sources.boolandStringtargets accept only their same-type sources. - Integer-to-integer casts check that the source fits in the target.
Floating-point-to-integer casts require a finite, in-range source and
truncate toward zero.
f64 as f32checks finite range before narrowing. truecasts to integer1, andfalsecasts to integer0.char, byte-string, C-string, unit, array, tuple, range, and closure values are not additional cast target names.- Number-to-
bool,bool-to-float, andString-to-number orString-to-boolcasts are not valid. - Casts associate from left to right, so
x as T as Uapplies the first cast before the second.
Edge cases
- Write
a < b && b < c, nota < b < c. - Call argument lists do not accept a trailing comma.
- Negative shift counts, counts larger than
u32, and counts outside the left operand’s width reportshift overflow. - Signed
+, signed binary-, signed unary-, and signed*overflows reportarithmetic overflow. - Integer
/and%reportdivision by zerowhen they fail. - Casts cannot target
char, byte-string, C-string, unit, array, tuple, range, or closure values.
Conditional expressions
CEL uses 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, andif ... 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
elsesupplies an implicit()branch, so the remaining branches must still be type-compatible with unit. - The braces belong to
ifsyntax. CEL does not use free-standing block expressions.
Edge cases
if flag { () } else if other { () }is valid without a finalelsebecause the omitted branch is also().
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
.0or.1. Named postfixes such asvalue.nameinvoke host-provided operations; they do not select tuple fields. - Arrays and tuples compose recursively over registered leaves, unit, other containers, and callables. Callable element types include their complete parameter and result shapes; storing a callable does not require equality.
Rust interoperability
CEL values map recursively to concrete Rust values:
| CEL shape | Rust value | Representation witness |
|---|---|---|
registered leaf T | T | Leaf<T> |
[T] | Array<R(T)> | ArrayOf<W(T)> |
() | CNil<()> | Nil |
(T, U) | CStackList<R(T), CStackList<R(U), CNil<()>>> | Cons<W(T), Cons<W(U), Nil>> |
Fn((A,), R) | Callable<CStackList<R(A), CNil<()>>, R(R)> | FnOf<Cons<W(A), Nil>, W(R)> |
Here R(T) is the recursively canonical value and W(T) its witness.
IntoCStackList and IntoRustTuple move between lists and ordinary Rust tuples
without requiring Clone, equality, or formatting. Logical list order is source
order.
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
.0i32is 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
|| bodydeclares a zero-parameter closure; spaced empty pipes| |are not this token.|name: Type| bodydeclares one typed parameter.|a: T, b: U| bodydeclares multiple typed parameters in source order.- An annotation uses the host’s configured type environment: built-in
scalars and generic types, registered cloneable leaves, recursive tuple
types, recursive array types, and structural
Fnsignatures. Container shapes require no separate registration. |pair: (i32, i32)| pair.0 + pair.1declares one tuple-typed parameter, not two separate parameters.- The closure body is one expression.
- A bare closure body extends to its enclosing expression boundary. For example,
|| 1 + 2returns3; 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| xis 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:
| Shorthand | Supported concrete types |
|---|---|
T | i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64 |
S | i8, i16, i32, i64, i128, isize, f32, f64 |
F | f32, f64 |
Each occurrence of a shorthand in one call denotes the same concrete type.
There is no implicit numeric conversion: for example, min(1i32, 2i64) has no
matching overload. Unsupported types and arities fail during expression
compilation, rather than producing the evaluation errors documented below.
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
| Parameter | Description |
|---|---|
a, b | Values to compare, both of the same type T. |
Return value
The smaller operand. For floats, exactly one NaN operand causes the other
operand to be returned; two NaNs produce NaN. Infinities compare in the usual
order. Equal operands may yield either operand, so comparing -0.0 and +0.0
does not guarantee a particular zero sign.
Complexity
O(1) time and space.
Example
min(3i32, -5i32)
Returns -5i32.
See also
max
max(a: T, b: T) -> T
Selects the larger of two same-type numeric values.
Parameters
| Parameter | Description |
|---|---|
a, b | Values to compare, both of the same type T. |
Return value
The larger operand. For floats, exactly one NaN operand causes the other
operand to be returned; two NaNs produce NaN. Infinities compare in the usual
order. Equal operands may yield either operand, so comparing -0.0 and +0.0
does not guarantee a particular zero sign.
Complexity
O(1) time and space.
Example
max(3.5f64, 2.5f64)
Returns 3.5f64.
See also
clamp
clamp(x: T, lo: T, hi: T) -> T
Restricts a numeric value to an inclusive interval.
Parameters
| Parameter | Description |
|---|---|
x | Value to restrict. |
lo | Inclusive lower bound, of the same type as x. |
hi | Inclusive upper bound, of the same type as x. |
Return value
lo if x < lo, hi if x > hi, otherwise x, all in the same type T.
Equal bounds are allowed. With valid floating-point bounds, a NaN x produces
NaN; infinite values and bounds follow the same comparisons. Because signed
zeros compare equal, an in-range zero retains the sign of x.
Errors
Reports invalid clamp bounds if lo <= hi is false: either lo > hi, or
either floating-point bound is NaN. Bounds are checked even when x is NaN.
Complexity
O(1) time and space.
Example
clamp(-2i32, 0i32, 10i32)
Returns 0i32.
See also
abs
abs(x: S) -> S
Computes an absolute value for a signed integer or float. Unsigned integers are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Signed integer or floating-point value of type S. |
Return value
The magnitude of x, in the same type. Floating-point -0.0 becomes +0.0,
either infinity becomes positive infinity, and NaN produces NaN.
Errors
Reports arithmetic overflow for the minimum value of any supported signed
integer type, because its positive magnitude is not representable.
Complexity
O(1) time and space.
Example
abs(-42i64)
Returns 42i64.
See also
signum
signum(x: S) -> S
Extracts a signed integer’s or float’s sign. Unsigned integers are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Signed integer or floating-point value of type S. |
Return value
For integers, returns -1, 0, or 1 according to whether x is negative,
zero, or positive, in the same integer type.
For floats, returns -1.0 for negative values (including negative infinity
and -0.0), 1.0 for positive values (including positive infinity and
+0.0), or NaN for NaN. The result has the same floating-point type as x.
In particular, floating-point zero does not produce zero.
Complexity
O(1) time and space.
Examples
signum(-7i32)
Returns -1i32.
signum(-3.5)
Returns -1.0 (f64).
See also
sqrt
sqrt(x: F) -> F
Computes a floating-point square root. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The nonnegative square root of a nonnegative value, in the same type.
Negative values, including negative infinity, produce NaN rather than an
evaluation error. NaN produces NaN; positive infinity remains positive
infinity. Signed zeros retain their signs, so sqrt(-0.0) returns -0.0.
Complexity
O(1) time and space.
Example
sqrt(9.0f32)
Returns 3.0f32.
See also
floor
floor(x: F) -> F
Rounds a float toward negative infinity. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The greatest integer less than or equal to x, represented in the same
floating-point type. Infinities are unchanged, NaN produces NaN, and signed
zeros retain their signs.
Complexity
O(1) time and space.
Example
floor(-3.2)
Returns -4.0 (f64).
See also
ceil
ceil(x: F) -> F
Rounds a float toward positive infinity. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The least integer greater than or equal to x, represented in the same
floating-point type. Infinities are unchanged, NaN produces NaN, and signed
zeros retain their signs. A value strictly between -1.0 and 0.0 rounds
to -0.0.
Complexity
O(1) time and space.
Example
ceil(-3.2)
Returns -3.0 (f64).
See also
floor, trunc, round_ties_even.
trunc
trunc(x: F) -> F
Rounds a float toward zero. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The integer part of x, represented in the same floating-point type.
Infinities are unchanged and NaN produces NaN. Signed zeros retain their
signs; a negative value strictly between -1.0 and 0.0 becomes -0.0.
Complexity
O(1) time and space.
Example
trunc(-3.2)
Returns -3.0 (f64).
See also
fract
fract(x: F) -> F
Extracts a float’s fractional part. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
x - trunc(x), in the same floating-point type. For finite nonintegral
values the fraction has the sign of x, not necessarily a positive sign.
Finite integral values, including either signed zero, produce +0.0.
Either infinity or NaN produces NaN rather than an evaluation error.
Complexity
O(1) time and space.
Example
fract(-3.75f32)
Returns -0.75f32.
See also
round_ties_even
round_ties_even(x: F) -> F
Rounds a float to the nearest integer, resolving exact halfway cases toward the even integer. Integer operands are not supported.
Parameters
| Parameter | Description |
|---|---|
x | Floating-point value of type F. |
Return value
The nearest integer, represented in the same floating-point type.
Infinities are unchanged and NaN produces NaN. Signed zeros retain their
signs; a negative input that rounds to zero produces -0.0.
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
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
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
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
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
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
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
atan2
atan2(y: F, x: F) -> F
Computes the angle of the vector (x, y), retaining its quadrant.
Parameters
| Parameter | Description |
|---|---|
y | The vertical coordinate, also the receiver in y.atan2(x). |
x | The 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
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
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
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
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
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
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
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
log
log(x: F, base: F) -> F
Computes the logarithm of x in a same-type base.
Parameters
| Parameter | Description |
|---|---|
x | The value whose logarithm is computed. |
base | The 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
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
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
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
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
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
Euclid library
The optional cel-euclid library provides concrete two-dimensional geometry
types and operations. A host installs it once on a fresh OpLookup with
cel_euclid::install. The core language does not install geometry implicitly.
Install cel-std separately for scalar operations;
either installation order preserves both libraries’ overloads.
Conventions
Every coordinate, extent, angle component, and matrix coefficient has type
f64. The Rust types use Euclid’s unknown units; CEL does not expose generic
unit parameters. Operand types must match the listed signatures exactly:
write 1.0, 1f64, or 1 as f64 where an operation requires f64.
There are no implicit numeric casts or return-type-only overloads.
Signatures use Rust-style parameter and return-type notation to describe CEL
calls, not to declare functions. T denotes only the concrete alternatives
listed with a signature; it is not CEL generic syntax.
Every named operation with a first argument also accepts receiver syntax:
name(value, args...) and value.name(args...) are equivalent. A unary
operation additionally accepts value.name. Only identity_transform()
has no receiver. Operators use their ordinary CEL spelling.
Except for normalize, inverse, and
outer_transformed_rect, operations preserve
Euclid’s ordinary floating-point behavior. Overflow, division by zero,
NaNs, and infinities do not by themselves produce evaluation errors.
Record construction does not validate finiteness, order bounds, or require
nonnegative sizes.
Types
Each record requires every listed field exactly once, in any order. Fields must have the indicated types. Missing, repeated, unknown, or incorrectly typed fields cause a parse error. Record construction takes constant time and space.
Point2D
A position with fields x: f64 and y: f64.
Point2D { x: 1.0, y: 2.0 }
// => Point2D { y: 2.0, x: 1.0 }
Adding a vector produces a point; subtracting two points produces a vector.
Access coordinates with x and y.
Vector2D
A displacement with fields x: f64 and y: f64.
Vector2D { x: 3.0, y: 4.0 }
// => Vector2D { y: 4.0, x: 3.0 }
Vectors support arithmetic, dot, cross,
length, and normalize.
Size2D
Two extents with fields width: f64 and height: f64.
Construction permits zero and negative extents.
Size2D { width: 4.0, height: 6.0 }
// => Size2D { height: 6.0, width: 4.0 }
Access extents with width and height.
Rect
An axis-aligned rectangle with fields origin: Point2D and size: Size2D.
Its maximum coordinates are the origin coordinates plus the extents.
Construction does not reorder negative extents.
Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}
// => Rect { size: Size2D { width: 4.0, height: 6.0 }, origin: Point2D { x: 1.0, y: 2.0 } }
See origin, size, and contains.
Box2D
An axis-aligned box with fields min: Point2D and max: Point2D.
Construction stores the supplied endpoints without sorting them.
Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}
// => Box2D { max: Point2D { x: 5.0, y: 8.0 }, min: Point2D { x: 1.0, y: 2.0 } }
See min and max, size, and contains.
Angle
An angle with one field, radians: f64. Construction does not wrap the
value to a particular interval.
Angle { radians: 1.5 }
// => radians(1.5)
Use degrees or radians to construct an angle
from a scalar or extract its magnitude.
Transform2D
An affine transform with six f64 fields:
m11, m12, m21, m22, m31, and m32.
It maps a point (x, y) to:
x' = m11 * x + m21 * y + m31
y' = m12 * x + m22 * y + m32
Transform2D {
m11: 1.0, m12: 0.0,
m21: 0.0, m22: 1.0,
m31: 0.0, m32: 0.0
}
// => identity_transform()
Use then to compose transforms. Vectors use only the linear
coefficients and ignore translation.
Operators
Equality: ==, !=
a: T == b: T -> bool
a: T != b: T -> bool
T is any one of the seven geometry types. Compares corresponding stored
components using f64 equality; it does not compare geometric equivalence.
Parameters
| Parameter | Description |
|---|---|
a, b | Geometry values of the same type. |
Return value
== returns true if all corresponding components compare equal.
!= returns its logical negation. A NaN component compares unequal even
to itself; positive and negative zero compare equal. Angles differing by
a full turn do not compare equal merely because they describe the same direction.
Complexity
Constant time and space.
Example
Point2D { x: 1.0, y: 2.0 } == Point2D { y: 2.0, x: 1.0 }
// => true
See also
Addition: +
point: Point2D + offset: Vector2D -> Point2D
a: Vector2D + b: Vector2D -> Vector2D
a: Size2D + b: Size2D -> Size2D
Adds corresponding components.
Parameters
| Parameter | Description |
|---|---|
point, offset | A position and the displacement to add. |
a, b | Two vectors or two sizes. |
Return value
A translated point, component-wise vector sum, or component-wise size sum. Vector-plus-point and point-plus-point are not supported.
Complexity
Constant time and space.
Example
Point2D { x: 1.0, y: 2.0 } + Vector2D { x: 3.0, y: 4.0 }
// => Point2D { x: 4.0, y: 6.0 }
See also
Subtraction: -
a: Point2D - b: Point2D -> Vector2D
point: Point2D - offset: Vector2D -> Point2D
a: Vector2D - b: Vector2D -> Vector2D
a: Size2D - b: Size2D -> Size2D
Subtracts corresponding components.
Parameters
| Parameter | Description |
|---|---|
a, b | Two points, two vectors, or two sizes. |
point, offset | A position and the displacement to subtract. |
Return value
For two points, the displacement from b to a. Otherwise, a point,
vector, or size containing the component-wise differences.
Complexity
Constant time and space.
Example
Point2D { x: 4.0, y: 6.0 } - Point2D { x: 1.0, y: 2.0 }
// => Vector2D { x: 3.0, y: 4.0 }
See also
+, distance_to.
Negation: unary -
-v: Vector2D -> Vector2D
Negates both components of a vector.
Parameters
| Parameter | Description |
|---|---|
v | The vector to negate. |
Return value
A vector with components -v.x and -v.y.
Complexity
Constant time and space.
Example
-Vector2D { x: 3.0, y: -4.0 }
// => Vector2D { x: -3.0, y: 4.0 }
See also
-.
Scaling: *, /
value: T * factor: f64 -> T
value: T / factor: f64 -> T
T is Vector2D or Size2D. Multiplies or divides each component by a scalar.
Parameters
| Parameter | Description |
|---|---|
value | A vector or size. |
factor | The scalar multiplier or divisor. |
Return value
A vector or size containing the scaled components. Scalar-left products
and point scaling are not supported. Division by zero follows f64
arithmetic rather than producing an evaluation error.
Complexity
Constant time and space.
Example
Vector2D { x: 3.0, y: 4.0 } * 2.0
// => Vector2D { x: 6.0, y: 8.0 }
Size2D { width: 4.0, height: 6.0 } / 2.0
// => Size2D { width: 2.0, height: 3.0 }
See also
Component and bounds access
is_finite
is_finite(value: T) -> bool
T is any one of the seven geometry types. Tests whether all stored scalar
components are finite.
Parameters
| Parameter | Description |
|---|---|
value | The geometry value to inspect. |
Return value
true if no stored component is NaN or infinite; otherwise false.
For Rect, this checks the origin and size, not the computed maximum
coordinates. It does not validate sizes, bounds ordering, or invertibility.
Complexity
Constant time and space.
Example
(Vector2D { x: 1.0, y: 2.0 } / 0.0).is_finite
// => false
See also
x, y
x(value: T) -> f64
y(value: T) -> f64
T is Point2D or Vector2D. Reads one coordinate or component.
Parameters
| Parameter | Description |
|---|---|
value | A point or vector. |
Return value
The stored x or y component, respectively.
Complexity
Constant time and space.
Example
(Point2D { x: 1.0, y: 2.0 }).y
// => 2.0
See also
width, height
width(value: T) -> f64
height(value: T) -> f64
T is Size2D, Rect, or Box2D. Reads one extent.
Parameters
| Parameter | Description |
|---|---|
value | A size, rectangle, or box. |
Return value
For sizes and rectangles, the corresponding stored size component.
For boxes, max.x - min.x or max.y - min.y. Negative extents are
returned unchanged rather than clamped to zero.
Complexity
Constant time and space.
Example
(Size2D { width: 4.0, height: 6.0 }).width
// => 4.0
See also
size, min_x, min_y, max_x, and max_y.
origin
origin(rect: Rect) -> Point2D
Reads a rectangle’s stored origin.
Parameters
| Parameter | Description |
|---|---|
rect | The rectangle to inspect. |
Return value
The origin field, without normalizing the rectangle.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).origin
// => Point2D { x: 1.0, y: 2.0 }
See also
size, min_x, min_y, max_x, and max_y.
size
size(value: Rect) -> Size2D
size(value: Box2D) -> Size2D
Returns both extents of a rectangle or box.
Parameters
| Parameter | Description |
|---|---|
value | A rectangle or box. |
Return value
For a rectangle, its stored size. For a box, the width and height
computed by subtracting min from max.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).size
// => Size2D { width: 4.0, height: 6.0 }
See also
min, max
min(bounds: Box2D) -> Point2D
max(bounds: Box2D) -> Point2D
Reads a box’s stored minimum or maximum point.
Parameters
| Parameter | Description |
|---|---|
bounds | The box to inspect. |
Return value
The stored min or max field. These overloads do not compute a
component-wise minimum or maximum of two geometry values.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).max
// => Point2D { x: 5.0, y: 8.0 }
See also
min_x, min_y, max_x, and max_y,
scalar min, scalar max.
min_x, min_y, max_x, max_y
min_x(value: T) -> f64
min_y(value: T) -> f64
max_x(value: T) -> f64
max_y(value: T) -> f64
T is Rect or Box2D. Reads one bound coordinate.
Parameters
| Parameter | Description |
|---|---|
value | A rectangle or box. |
Return value
For rectangles, minimum coordinates are the origin components; maximum
coordinates add the corresponding size components. For boxes, these
operations read the components of the stored min or max point.
They do not reorder inverted bounds.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).max_y
// => 8.0
See also
min and max, width and height.
center
center(value: T) -> Point2D
T is Rect or Box2D. Computes the center of the bounds.
Parameters
| Parameter | Description |
|---|---|
value | A rectangle or box. |
Return value
The midpoint computed using Euclid’s floating-point arithmetic, without validating or normalizing the bounds.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).center
// => Point2D { x: 3.0, y: 5.0 }
See also
Vector queries
dot
dot(a: Vector2D, b: Vector2D) -> f64
Computes the dot product of two vectors.
Parameters
| Parameter | Description |
|---|---|
a, b | The vectors to multiply. |
Return value
a.x * b.x + a.y * b.y, subject to floating-point rounding.
Complexity
Constant time and space.
Example
dot(Vector2D { x: 3.0, y: 4.0 }, Vector2D { x: 1.0, y: 2.0 })
// => 11.0
See also
cross
cross(a: Vector2D, b: Vector2D) -> f64
Computes the scalar two-dimensional cross product.
Parameters
| Parameter | Description |
|---|---|
a, b | The vectors, in operand order. |
Return value
a.x * b.y - a.y * b.x, subject to floating-point rounding.
Complexity
Constant time and space.
Example
cross(Vector2D { x: 3.0, y: 4.0 }, Vector2D { x: 1.0, y: 2.0 })
// => 2.0
See also
dot.
square_length
square_length(v: Vector2D) -> f64
Computes the squared length of a vector.
Parameters
| Parameter | Description |
|---|---|
v | The vector to measure. |
Return value
v.x * v.x + v.y * v.y, subject to floating-point rounding.
Intermediate products can overflow or underflow.
Complexity
Constant time and space.
Example
(Vector2D { x: 3.0, y: 4.0 }).square_length
// => 25.0
See also
length
length(v: Vector2D) -> f64
Computes a vector’s Euclidean length.
Parameters
| Parameter | Description |
|---|---|
v | The vector to measure. |
Return value
The square root of the squared length, subject to floating-point rounding, overflow, and underflow. This operation does not reject non-finite results.
Complexity
Constant time and space.
Example
(Vector2D { x: 3.0, y: 4.0 }).length
// => 5.0
See also
square_length, normalize, distance_to.
normalize
normalize(v: Vector2D) -> Vector2D
Returns a vector in the direction of v with unit length, subject to
floating-point rounding.
Parameters
| Parameter | Description |
|---|---|
v | The vector to normalize. |
Return value
A new vector obtained by dividing each component of v by its computed length.
Errors
Evaluation fails if the computed length is zero or non-finite, or if either result component is non-finite. Finite, nonzero components do not guarantee success: computing the length can underflow to zero or overflow to infinity.
Complexity
Constant time and space.
Example
normalize(Vector2D { x: 3.0, y: 4.0 })
// => Vector2D { x: 0.6, y: 0.8 }
Equivalent receiver-call syntax:
(Vector2D { x: 3.0, y: 4.0 }).normalize()
// => Vector2D { x: 0.6, y: 0.8 }
A zero vector produces an evaluation error:
normalize(Vector2D { x: 0.0, y: 0.0 })
// Evaluation error: normalize requires a positive finite length
See also
length, square_length, is_finite.
distance_to
distance_to(a: Point2D, b: Point2D) -> f64
Computes the Euclidean distance between two points.
Parameters
| Parameter | Description |
|---|---|
a, b | The points to measure between. |
Return value
The length of their displacement, subject to floating-point rounding, overflow, and underflow.
Complexity
Constant time and space.
Example
(Point2D { x: 1.0, y: 2.0 }).distance_to(Point2D { x: 4.0, y: 6.0 })
// => 5.0
See also
Bounds operations
translate
translate(value: T, offset: Vector2D) -> T
T is Rect or Box2D. Moves bounds by a displacement.
Parameters
| Parameter | Description |
|---|---|
value | The rectangle or box to move. |
offset | The displacement to add. |
Return value
For a rectangle, a translated origin with the original size. For a box, translated minimum and maximum points.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).translate(Vector2D { x: 3.0, y: 4.0 })
// => Rect { origin: Point2D { x: 4.0, y: 6.0 }, size: Size2D { width: 4.0, height: 6.0 } }
See also
+, translation.
inflate
inflate(value: T, dx: f64, dy: f64) -> T
T is Rect or Box2D. Expands bounds on both sides of each axis.
Parameters
| Parameter | Description |
|---|---|
value | The rectangle or box to expand. |
dx, dy | Expansion on each horizontal and vertical side. Negative values shrink. |
Return value
Bounds with minimum coordinates decreased by dx, dy and maximum
coordinates increased by dx, dy. The width changes by 2 * dx and
the height by 2 * dy. Excessive shrinking can produce negative extents;
it is not an evaluation error.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).inflate(1.0, 2.0)
// => Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 6.0, y: 10.0 } }
See also
to_box2d
to_box2d(rect: Rect) -> Box2D
Converts an origin-and-size rectangle to endpoint bounds.
Parameters
| Parameter | Description |
|---|---|
rect | The rectangle to convert. |
Return value
A box whose min is the rectangle’s origin and whose max is the origin
plus the size. The conversion does not normalize negative extents.
Complexity
Constant time and space.
Example
(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
}).to_box2d
// => Box2D { min: Point2D { x: 1.0, y: 2.0 }, max: Point2D { x: 5.0, y: 8.0 } }
See also
to_rect
to_rect(bounds: Box2D) -> Rect
Converts endpoint bounds to an origin-and-size rectangle.
Parameters
| Parameter | Description |
|---|---|
bounds | The box to convert. |
Return value
A rectangle whose origin is min and whose size is max - min.
Floating-point rounding means conversions need not round-trip exactly.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).to_rect
// => Rect { origin: Point2D { x: 1.0, y: 2.0 }, size: Size2D { width: 4.0, height: 6.0 } }
See also
contains
contains(bounds: Rect, point: Point2D) -> bool
contains(bounds: Box2D, point: Point2D) -> bool
Tests point containment using half-open bounds.
Parameters
| Parameter | Description |
|---|---|
bounds | The rectangle or box to test. |
point | The point to locate. |
Return value
true when both minimum coordinates are included and both maximum
coordinates are excluded:
min_x <= point.x < max_x and min_y <= point.y < max_y.
Empty or inverted bounds contain no points.
Complexity
Constant time and space.
Example
(Box2D {
min: Point2D { x: 1.0, y: 2.0 },
max: Point2D { x: 5.0, y: 8.0 }
}).contains(Point2D { x: 5.0, y: 4.0 })
// => false
See also
contains_rect, contains_box, intersects.
contains_rect
contains_rect(outer: Rect, inner: Rect) -> bool
Tests whether one rectangle contains another using Euclid’s bounds rules.
Parameters
| Parameter | Description |
|---|---|
outer | The containing rectangle. |
inner | The rectangle to test. |
Return value
true if inner is empty, or if outer is nonempty and contains all of
inner’s bounds. Coincident maximum edges are allowed for nonempty rectangles,
unlike point containment. NaN extents follow Euclid’s empty-bounds rules.
Complexity
Constant time and space.
Example
contains_rect(
Rect { origin: Point2D { x: 0.0, y: 0.0 }, size: Size2D { width: 10.0, height: 10.0 } },
Rect { origin: Point2D { x: 1.0, y: 2.0 }, size: Size2D { width: 4.0, height: 6.0 } }
)
// => true
See also
contains_box
contains_box(outer: Box2D, inner: Box2D) -> bool
Tests whether one box contains another using Euclid’s bounds rules.
Parameters
| Parameter | Description |
|---|---|
outer | The containing box. |
inner | The box to test. |
Return value
true if inner is empty, or if outer is nonempty and contains all of
inner’s bounds. Coincident maximum edges are allowed for nonempty boxes.
NaN endpoints follow Euclid’s empty-bounds rules.
Complexity
Constant time and space.
Example
contains_box(
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 10.0, y: 10.0 } },
Box2D { min: Point2D { x: 1.0, y: 2.0 }, max: Point2D { x: 5.0, y: 8.0 } }
)
// => true
See also
intersects
intersects(a: T, b: T) -> bool
T is Rect or Box2D. Tests whether two bounds overlap.
Parameters
| Parameter | Description |
|---|---|
a, b | Two rectangles or two boxes. |
Return value
true when both axes satisfy the strict endpoint comparisons
a.min < b.max and a.max > b.min. For nonempty finite bounds, this
means positive-area overlap; touching at an edge or corner returns false.
The operation does not separately reject empty or inverted bounds: an
empty box strictly inside another box can return true.
Complexity
Constant time and space.
Example
intersects(
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 2.0, y: 2.0 } },
Box2D { min: Point2D { x: 2.0, y: 0.0 }, max: Point2D { x: 4.0, y: 2.0 } }
)
// => false
intersects(
Box2D { min: Point2D { x: 1.0, y: 1.0 }, max: Point2D { x: 1.0, y: 1.0 } },
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 2.0, y: 2.0 } }
)
// => true
See also
union
union(a: T, b: T) -> T
T is Rect or Box2D. Computes enclosing axis-aligned bounds.
Parameters
| Parameter | Description |
|---|---|
a, b | Two rectangles or two boxes. |
Return value
The enclosing bounds under Euclid’s union rules. For nonempty finite inputs, the minimum coordinates are the component-wise minima and the maximum coordinates are the component-wise maxima. If one operand is empty, its bounds are ignored; if both are empty, the first operand supplies the bounds. Rectangles convert through endpoint bounds, so their extents can incur rounding. This is not a shape-set union.
Complexity
Constant time and space.
Example
union(
Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 2.0, y: 2.0 } },
Box2D { min: Point2D { x: 1.0, y: 1.0 }, max: Point2D { x: 4.0, y: 3.0 } }
)
// => Box2D { min: Point2D { x: 0.0, y: 0.0 }, max: Point2D { x: 4.0, y: 3.0 } }
See also
intersects, outer_transformed_rect.
Angles
degrees
degrees(value: f64) -> Angle
degrees(angle: Angle) -> f64
Constructs an angle from degrees or expresses an angle in degrees.
Parameters
| Parameter | Description |
|---|---|
value | An angle magnitude in degrees. |
angle | An angle to convert to a scalar. |
Return value
For f64, an angle stored in radians. For Angle, its magnitude in degrees.
Neither overload wraps the angle to a particular interval.
Complexity
Constant time and space.
Example
degrees(0.0).radians
// => 0.0
See also
radians
radians(value: f64) -> Angle
radians(angle: Angle) -> f64
Constructs an angle from radians or reads its stored magnitude.
Parameters
| Parameter | Description |
|---|---|
value | An angle magnitude in radians. |
angle | An angle whose magnitude is requested. |
Return value
For f64, an angle with the given stored radians. For Angle, its stored
radians component. Neither overload wraps the angle.
Complexity
Constant time and space.
Example
radians(1.5).radians
// => 1.5
See also
Affine transforms
m11, m12, m21, m22, m31, m32
m11(transform: Transform2D) -> f64
m12(transform: Transform2D) -> f64
m21(transform: Transform2D) -> f64
m22(transform: Transform2D) -> f64
m31(transform: Transform2D) -> f64
m32(transform: Transform2D) -> f64
Reads a stored affine matrix coefficient.
Parameters
| Parameter | Description |
|---|---|
transform | The transform to inspect. |
Return value
The coefficient named by the operation. m31 and m32 are translation
components; the other four form the linear part.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 20.0 }).m31
// => 10.0
See also
identity_transform
identity_transform() -> Transform2D
Constructs the identity affine transform.
Return value
A transform with m11 = m22 = 1.0 and all other coefficients zero.
It leaves finite points and vectors unchanged.
Complexity
Constant time and space.
Example
identity_transform().transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 1.0, y: 2.0 }
See also
translation
translation(offset: Vector2D) -> Transform2D
Constructs a translation transform.
Parameters
| Parameter | Description |
|---|---|
offset | The displacement to apply to points. |
Return value
A transform with identity linear coefficients and translation components
offset.x, offset.y. Applying it to a vector ignores the translation.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 20.0 })
.transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 11.0, y: 22.0 }
See also
translate, then, transform_vector.
scale
scale(sx: f64, sy: f64) -> Transform2D
Constructs an axis-aligned scaling transform about the origin.
Parameters
| Parameter | Description |
|---|---|
sx, sy | Horizontal and vertical scale factors. |
Return value
A transform with m11 = sx, m22 = sy, and all other coefficients zero.
Negative factors reflect the corresponding axes; zero factors produce a
singular transform. Construction does not reject either case.
Complexity
Constant time and space.
Example
scale(2.0, 3.0).transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 2.0, y: 6.0 }
See also
rotation
rotation(angle: Angle) -> Transform2D
Constructs a rotation about the origin.
Parameters
| Parameter | Description |
|---|---|
angle | The rotation angle. |
Return value
A transform with linear coefficients cos(angle), sin(angle),
-sin(angle), cos(angle) and zero translation.
Positive angles rotate (1, 0) toward (0, 1); the visual direction
depends on whether the consuming coordinate system’s y-axis points up or down.
Complexity
Constant time and space.
Example
rotation(degrees(0.0)).transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 1.0, y: 2.0 }
See also
then
then(first: Transform2D, second: Transform2D) -> Transform2D
Composes transforms in application order: first, then second.
Parameters
| Parameter | Description |
|---|---|
first | The transform applied first. |
second | The transform applied second. |
Return value
The composed affine transform. Composition generally does not commute. Its result can differ from two separately evaluated mappings because floating-point operations round at different points.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 0.0 })
.then(scale(2.0, 2.0))
.transform_point(Point2D { x: 1.0, y: 0.0 })
// => Point2D { x: 22.0, y: 0.0 }
See also
translation, scale, transform_point.
determinant
determinant(transform: Transform2D) -> f64
Computes the determinant of the linear part of an affine transform.
Parameters
| Parameter | Description |
|---|---|
transform | The transform to inspect. |
Return value
m11 * m22 - m12 * m21, subject to floating-point rounding.
Translation does not affect the result. A zero determinant indicates a
singular linear part; a nonzero result alone does not guarantee that
checked inversion succeeds.
Complexity
Constant time and space.
Example
scale(2.0, 3.0).determinant
// => 6.0
See also
inverse
inverse(transform: Transform2D) -> Transform2D
Computes a checked affine inverse.
Parameters
| Parameter | Description |
|---|---|
transform | The transform to invert. |
Return value
Euclid’s inverse with finite coefficients and no nonzero exact cofactor lost to a zero inverse coefficient. Exact-zero cofactors and nonzero subnormal coefficients are allowed.
The check does not guarantee exact-real arithmetic, numerical stability, or an exact round trip. Other rounding and conditioning errors retain Euclid’s floating-point behavior.
Errors
Evaluation fails for:
- Non-finite input coefficients.
- A zero or non-finite computed determinant.
- Non-finite inverse coefficients.
- A zero inverse coefficient whose exact cofactor is nonzero, including loss within translation-cofactor arithmetic before determinant scaling.
Complexity
Constant time and space.
Example
scale(2.0, 4.0).inverse().transform_point(Point2D { x: 6.0, y: 8.0 })
// => Point2D { x: 3.0, y: 2.0 }
See also
transform_point
transform_point(transform: Transform2D, point: Point2D) -> Point2D
Applies an affine transform to a point, including translation.
Parameters
| Parameter | Description |
|---|---|
transform | The mapping to apply. |
point | The input position. |
Return value
The point given by the coordinate equations in Transform2D. Mapping is unchecked and can return non-finite coordinates.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 0.0 })
.transform_point(Point2D { x: 1.0, y: 2.0 })
// => Point2D { x: 11.0, y: 2.0 }
See also
transform_vector, outer_transformed_rect.
transform_vector
transform_vector(transform: Transform2D, vector: Vector2D) -> Vector2D
Applies the linear part of an affine transform to a displacement.
Parameters
| Parameter | Description |
|---|---|
transform | The mapping to apply. |
vector | The input displacement. |
Return value
A vector with components m11 * x + m21 * y and m12 * x + m22 * y.
The translation coefficients m31, m32 do not contribute.
Mapping is unchecked and can return non-finite components.
Complexity
Constant time and space.
Example
translation(Vector2D { x: 10.0, y: 20.0 })
.transform_vector(Vector2D { x: 3.0, y: 4.0 })
// => Vector2D { x: 3.0, y: 4.0 }
See also
outer_transformed_rect
outer_transformed_rect(transform: Transform2D, rect: Rect) -> Rect
Computes checked axis-aligned bounds enclosing all four transformed corners.
Parameters
| Parameter | Description |
|---|---|
transform | The affine mapping, including rotation, shear, or reflection. |
rect | The source rectangle. |
Return value
A finite rectangle enclosing all four corners after Euclid maps them.
If reconstructing a maximum from an origin and extent rounds inward,
the extent advances by one representable f64 step to enclose that maximum.
Zero source extents and singular transforms are allowed.
An outer bound is not an interior-containment test: its corners need not lie inside the transformed shape.
Errors
Evaluation fails for:
- Non-finite matrix coefficients, source origins, extents, or endpoints.
- Negative source extents, or positive extents whose computed endpoints do not advance beyond the corresponding origin.
- Projected edges that overflow or lose a nonzero extent completely to underflow, or non-finite projected spans.
- Non-finite mapped corners, or a positive projected span that collapses to zero after mapping.
- Output extents or enclosing endpoints that cannot be represented finitely.
Failure produces an evaluation error, not an empty rectangle.
Complexity
Constant time and space; maps four corners.
Example
scale(2.0, 3.0).outer_transformed_rect(Rect {
origin: Point2D { x: 1.0, y: 2.0 },
size: Size2D { width: 4.0, height: 6.0 }
})
// => Rect { origin: Point2D { x: 2.0, y: 6.0 }, size: Size2D { width: 8.0, height: 18.0 } }
See also
transform_point, union, inverse.
Host integration
This 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
| Category | Forms | Notes |
|---|---|---|
| Identifiers | name, value2, user_name | Bare names for values, callees, and closure parameters. |
| Reserved words | if, else, as, true, false | These spellings have fixed language roles. |
| Literal tokens | integers, floats, strings, characters, bytes, byte strings, C strings | Unit () comes from grammar, not from a standalone literal token. |
| Delimiters | ( ) [ ] { } | Used for grouping, tuples, calls, arrays, and if branches. |
| Structural punctuation | , : . | Used for separators, closure parameter types, and tuple indexing. |
| Operators | + - * / % ! && || == != < <= > >= & | ^ << >> .. ..= as | See the operator tables below. |
Type table
| Kind | Names or forms | Notes |
|---|---|---|
| Built-in scalar types | i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String | These are the named cast targets. Closure annotations also accept registered cloneable leaves and built-in generic types through the configured type resolver. |
| Other value forms | char, byte strings, C strings, unit, tuples, arrays, ranges, closures | Expressions can use these values, but they are not extra built-in scalar type names. |
| Structural types | (), (T,), (T, U), [T], Fn((A, B), R) | Compose recursively without registering each aggregate. (T) groups a type. Fn((), R) has no parameters; Fn((T,), R) has one. |
Literal table
| Form | Example | Result |
|---|---|---|
| Integer | 42, 42u16 | Unsuffixed integers default to i32. Integer suffixes are i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, and usize. |
| Floating-point | 3.5, 6.02e23f32 | Unsuffixed floating-point literals default to f64. Floating-point suffixes are f32 and f64. |
| Boolean | true, false | Produces bool. |
| String | "hello" | Produces String. |
| Character | 'a' | Produces char. |
| Byte | b'A' | Produces u8. |
| Byte string | b"bytes" | Produces a byte-string value. |
| C string | c"header" | Produces a C-string value. |
| Unit | () | Produces the unit value. |
Operator precedence table
| Level (low to high) | Forms | Associativity |
|---|---|---|
| 1 | range forms | non-chaining |
| 2 | || | left |
| 3 | && | left |
| 4 | ==, !=, <, <=, >, >= | non-chaining |
| 5 | | | left |
| 6 | ^ | left |
| 7 | & | left |
| 8 | <<, >> | left |
| 9 | +, - | left |
| 10 | *, /, % | left |
| 11 | as | left |
| 12 | unary -, ! | prefix |
| 13 | calls, .N, .name, .name(args) | left |
Operator rules table
| Forms | Accepted operands | Notes |
|---|---|---|
+ | homogeneous numeric operands, or String + String | Unsigned integers wrap. Signed integers report arithmetic overflow on overflow. Strings concatenate. |
binary - | homogeneous numeric operands | Unsigned integers wrap. Signed integers report arithmetic overflow on overflow. |
unary - | signed integers and floating-point values | Signed integer overflow reports arithmetic overflow. |
* | homogeneous numeric operands | Unsigned integers wrap. Signed integers report arithmetic overflow on overflow. |
/, % | homogeneous numeric operands | Integer failures report division by zero. |
&, |, ^ | homogeneous integer operands | Integer-only bitwise operators. |
<<, >> | integer left operand and integer right operand | The right operand must fit u32, and the shift count must be in range. Failures report shift overflow. |
! | bool | Logical negation. |
&&, || | bool and bool | Short-circuit logical operators. |
==, != | homogeneous numeric operands, homogeneous bool, or homogeneous String | Produces bool. |
<, <=, >, >= | homogeneous numeric operands or homogeneous String | Produces bool. |
| range forms | homogeneous numeric endpoints, or no endpoints for .. | ..= requires a right endpoint. Group closures and nested ranges used as endpoints. |
as | expression plus built-in scalar target name | Integer targets accept integer, floating-point, and bool sources. f32/f64 targets accept integer and floating-point sources. bool and String targets accept only their same-type sources. |
call and .N | a valid callee and arguments, or a tuple plus an unsuffixed integer | .N applies to tuples only. |
Numeric call table
All functions in this table belong to the standard library and are available when the evaluation environment installs it. See Standard library for worked examples and fuller prose.
| Function | Accepted operands | Result | Notes |
|---|---|---|---|
min(a, b), max(a, b) | same-type numeric operands: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64 | same as operands | No coercion. Successful calls are infallible. |
clamp(x, lo, hi) | same-type numeric operands from the same 14-type set | same as operands | Requires ordered bounds with lo <= hi; otherwise reports invalid clamp bounds. |
abs(x) | i8, i16, i32, i64, i128, isize, f32, f64 | same as operand | Minimum signed integer reports arithmetic overflow. |
signum(x) | i8, i16, i32, i64, i128, isize, f32, f64 | same as operand | No unsigned support. Successful calls are infallible. |
sqrt(x) | f32, f64 | same as operand | Float-only. Inputs below zero yield NaN rather than an error. |
floor(x), ceil(x), trunc(x), fract(x), round_ties_even(x), round(x) | f32, f64 | same as operand | round resolves halfway cases away from zero; round_ties_even selects the even neighbor. |
sin(x), cos(x), tan(x) | f32, f64 | same as operand | Angles in radians. Infinite inputs yield NaN. |
asin(x), acos(x), atan(x) | f32, f64 | same as operand | Results in radians. asin and acos yield NaN outside [-1, 1]. |
atan2(y, x) | same-type f32 or f64 | same as operands | Quadrant-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, f64 | same as operand | Angle conversion without wrapping. |
hypot(x, y) | same-type f32 or f64 | same as operands | Vector length. |
cbrt(x) | f32, f64 | same as operand | Real cube root, including negative inputs. |
powi(x, exponent) | f32 or f64 base, i32 exponent | same as base | Supports negative integer exponents. |
powf(x, exponent) | same-type f32 or f64 | same as operands | Floating-point power. |
exp(x), exp2(x) | f32, f64 | same as operand | Powers of e and 2. |
ln(x), log2(x), log10(x) | f32, f64 | same as operand | Logarithms in bases e, 2, and 10; zeros yield negative infinity and negative inputs yield NaN. |
log(x, base) | same-type f32 or f64 | same as operands | Arbitrary-base logarithm with Rust’s floating-point domain behavior. |
recip(x) | f32, f64 | same as operand | Reciprocal; signed zeros yield signed infinities. |
mul_add(x, multiplier, addend) | same-type f32 or f64 | same as operands | Fused multiply-add with one rounding step. |
rem_euclid(x, divisor) | same-type f32 or f64 | same as operands | Nonnegative remainder; rounding can produce abs(divisor). Zero divisor yields NaN. |
copysign(magnitude, sign) | same-type f32 or f64 | same as operands | Copies 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
| Form | Syntax | Rules |
|---|---|---|
| Unit | () | The empty parenthesized form is the unit value. |
| Grouping | (expr) | Changes grouping without creating a tuple. |
| Tuple | (expr,), (a, b, ...) | Positional value form. A 1-tuple requires the trailing comma. |
| Array | [a, b, ...] | Unannotated arrays are non-empty, homogeneous, and comma-separated. |
| Typed array | [a, b]: [T], []: [T] | The annotation states the complete array type and permits empty arrays. |
| Nested array | [[0], [1]], []: [[i32]] | Each element must still have the same array element type. |
| Tuple array | [(0, true)], []: [(i32, bool)] | Each positional type participates in homogeneity. |
| Callable array | []: [Fn((i32,), [i32])] | Empty arrays retain full signatures; equality is not required for storage. |
| Range | a..b, a..=b, a.., ..b, ..=b, .. | Endpoint-bearing forms require homogeneous numeric endpoints. |
Closure table
| Form | Meaning | Notes |
|---|---|---|
|| expr | zero-parameter closure | The body is one expression. |
|x: T| expr | one typed parameter | T resolves through the configured type environment and may recursively contain tuples or arrays. |
|x: T, y: U| expr | multiple typed parameters | Parameters are comma-separated and ordered left to right. |
|pair: (i32, i32)| expr | one tuple-typed parameter | Tuple parameters are still one parameter and use .N inside the body. |
| nested tuple type | |value: (i32, (f64, bool))| expr | Tuple types may nest recursively. |
(|x: f64| x)(1.0) | immediate invocation | CEL checks exact parameter/result shapes; arguments evaluate once, left to right. Unknown callable signatures fail explicitly. Host call operations retain precedence. |
Exact rules
- Calls accept zero or more comma-separated arguments and do not accept a trailing comma.
- Tuple indexing uses
.Nwith an unsuffixed integer and may be chained. - Unannotated arrays are non-empty and homogeneous. A complete type annotation permits an empty array and must match the element type exactly.
- Tuples and arrays are distinct forms and compose recursively, including registered leaves, source unit, and callable values.
- Rust interoperability uses
Array<T>, source-ordered canonical lists (Nil/Conswitnesses), andCallable<Args, R>(FnOfwitness).Array<Array<T>>converts toVec<Array<T>>, not allocation-preserving deep native vectors. See collections. - Arbitrary erased external values require complete shape checks.
PreparedSegmentandprepared_segment::PreparedRawSegmentvalidate at preparation, not each invocation. Clone, equality, and formatting are explicit independent capabilities. - Native Rust operations can serve interpreted CEL through typed adapters.
- 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.nameandexpr.name()invoke the same host-provided unary operation.expr.name(a, b)passes the receiver first, followed by explicit arguments. Each operand evaluates once in source order. A value binding namednamedoes not hide a dot operation, and the member label is not a variable reference.(expr.name)()calls the operation’s result normally; it fails if the result is not callable. Dot operations, ordinary calls, and tuple indexing share postfix precedence and may be chained.TypeName { field: expression, ... }constructs a host-registered record. Constructor names belong to the type namespace, separate from value names. Field labels are not variable references. Every schema field is required exactly once; unknown, duplicate, missing, and incorrectly typed fields are errors. Field expressions evaluate once in written order, independent of schema order. An empty schema permitsTypeName {}. Field lists do not permit trailing commas.- Field types match exactly: an
f64field accepts1.0, not thei32literal1. The static checker uses installed record schemas and operation signatures; known custom types retain their identities instead of unifying with unrelated types. Unknown constructors are errors. Unresolved inputs and calls without metadata remain best-effort, but all their child expressions are still checked. - Unparenthesized record constructors are forbidden throughout an
ifcondition. Parentheses, call arguments, tuple elements, and array elements start unrestricted expression positions: writeif (Sample { value: 3.0 }.value) > 0.0 { ... }. The restriction propagates through operators, ranges, and closure bodies. - Members are reads/operations, not assignment targets. CEL has no member assignment.
- CEL does not use bracket postfix indexing such as
expr[0]. - CEL does not define map literals, object literals, or a ternary operator.
- A closure body resolves its own parameters and does not capture free variables from the enclosing expression.
- A one-element tuple type requires
(T,);(T)is grouping.
Edge cases
- Signed
+, signed binary-, signed unary-, and signed*overflow reportarithmetic overflow. - Integer
/and%failures reportdivision by zero. - Negative, too-large, or non-
u32-fitting shift counts reportshift overflow. a < b < cis not valid; writea < b && b < c.1..2..3is not valid...=is not valid without a right endpoint.[]is not valid.[(0, 1), (true, 2)]is not valid because element shapes differ.