Closures
CEL uses |...| for closure literals. A closure is an expression and can
appear anywhere an expression is expected. For the exact grammar, see the
Reference Manual.
Concept
A closure packages a parameter list and a single expression body into a callable value.
Syntax
closure_expression = ("||" | "|" [ closure_param { "," closure_param } ] "|")
expression .
closure_param = identifier ":" type_expression .
type_expression = identifier [ "(" type_expression { "," type_expression } ")" ]
| "[" type_expression "]"
| "(" [ type_expression [ "," [ type_expression { "," type_expression } ] ] ] ")" .
Worked examples
|x: i32| x + 1
|pair: (i32, i32)| pair.0 + pair.1
|| 1..=5
(|x: f64| x)(1.0)
(|pair: (i32, i32)| pair.0 + pair.1)((2, 3))
(|rows: [[i32]]| rows)([[1], [2]])
|f: Fn((i32,), [i32])| f(42)
[]: [Fn((i32,), [i32])]
Exact rules
Closures:
|| bodydeclares a zero-parameter closure.|name: Type| bodydeclares one typed parameter.|a: T, b: U| bodydeclares multiple typed parameters in source order.- An annotation uses the host’s configured type environment: built-in
scalars and generic types, registered cloneable leaves, recursive tuple
types, recursive array types, and structural
Fnsignatures. Container shapes require no separate registration. |pair: (i32, i32)| pair.0 + pair.1declares one tuple-typed parameter, not two separate parameters.- The closure body is one expression.
- A closure body resolves its own parameters and does not capture free variables from an enclosing expression. Installed library operations remain available inside it.
- Parenthesize a closure to invoke it immediately. CEL checks the exact parameter and result shapes, including nested tuples and array element types. Arguments evaluate once, left to right; body errors propagate to the enclosing evaluation.
- Known signatures travel through nested calls and tuple intermediates. Conditional closure alternatives must have identical known signatures. An opaque host-provided closure with no known signature reports an unsupported-call error rather than guessing a result type. A host may still supply its own call operation, which takes precedence.
Host registration
CELParser::with_type_resolver supplies annotation lookup. A registered
leaf uses ValueType::cloneable_leaf::<T>() so parameters can clone
owned values without requiring Copy, equality, or debug formatting.
Adam supplies these descriptors through its existing TypeRegistry;
no second closure-specific type registry is needed. Results move from
the body into the caller and need no clone adapter of their own.
The Rust DynClosure::new, call, and call_boxed APIs remain available
for existing host integrations. DynClosure::typed and
DynSegment::push_closure additionally retain the complete metadata
needed by general CEL invocation.
Typed callable interoperability
Fn((A, B), R) has two parameters, Fn((A,), R) has one, and Fn((), R)
has none. The parameter list is always a tuple type: Fn(A, R) does not
stand for one parameter. A parameter can itself be a tuple, array, or callable.
Arrays compare whole signatures when checking element homogeneity.
Callable<Args, R> uses concrete canonical list arguments and an owned result.
FnOf<ArgsWitness, ResultWitness> supplies its complete structural witness.
Callable::from_fn binds a native function pointer;
Callable::from_prepared binds a matching PreparedSegment. Both expose the
same call(&args) interface with no signature comparisons during invocation.
Clones share execution state; recursive entry into the same carrier returns an
error. Body errors propagate. This interface prepares for a future compiled
backend; that backend is not implemented.
This example checks a typed empty array and calls native and interpreted functions through the same Rust interface. Callables need no equality capability to be array elements:
#![allow(unused)]
fn main() {
/// Preserves complete callable signatures even in empty arrays, without equality.
#[test]
fn typed_callable_arrays() {
use cel_parser::{CELParser, OpLookup};
use cel_runtime::{
Array, ArrayOf, CNil, CStackList, Callable, Cons, FnOf, IntoCStackList, Leaf, Nil,
};
type Args = Cons<Leaf<i32>, Nil>;
type Function = FnOf<Args, ArrayOf<Leaf<i32>>>;
let mut parser = CELParser::new(OpLookup::new());
let empty = parser.parse_str("[]: [Fn((i32,), [i32])]").unwrap();
let mut empty = empty.prepare::<Nil, ArrayOf<Function>>().unwrap();
assert!(empty.call(&().into_c_stack_list()).unwrap().is_empty());
let body = parser.parse_str("[|x: i32| [x]]").unwrap();
let mut body = body.prepare::<Nil, ArrayOf<Function>>().unwrap();
let interpreted = body.call(&().into_c_stack_list()).unwrap();
let native =
Callable::from_fn(|args: &CStackList<i32, CNil<()>>| Ok(Array::from_vec(vec![args.1])));
for function in [native, interpreted.into_vec().pop().unwrap()] {
assert_eq!(
function
.call(&(42,).into_c_stack_list())
.unwrap()
.as_slice(),
[42]
);
}
}
}
Edge cases
- Closure parameter lists do not accept a trailing comma.
(i32,)is a one-element tuple type;(i32)groups the scalar type.- Closure bodies do not introduce statement blocks or local binding syntax.