Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Closures

CEL uses |...| for closure literals. A closure is an expression and can appear anywhere an expression is expected. For the exact grammar, see the Reference Manual.

Concept

A closure packages a parameter list and a single expression body into a callable value.

Syntax

closure_expression = ("||" | "|" [ closure_param { "," closure_param } ] "|")
                     expression .
closure_param = identifier ":" type_expression .
type_expression = identifier [ "(" type_expression { "," type_expression } ")" ]
                | "[" type_expression "]"
                | "(" [ type_expression [ "," [ type_expression { "," type_expression } ] ] ] ")" .

Worked examples

|x: i32| x + 1
|pair: (i32, i32)| pair.0 + pair.1
|| 1..=5
(|x: f64| x)(1.0)
(|pair: (i32, i32)| pair.0 + pair.1)((2, 3))
(|rows: [[i32]]| rows)([[1], [2]])
|f: Fn((i32,), [i32])| f(42)
[]: [Fn((i32,), [i32])]

Exact rules

Closures:

  • || body declares a zero-parameter closure.
  • |name: Type| body declares one typed parameter.
  • |a: T, b: U| body declares multiple typed parameters in source order.
  • An annotation uses the host’s configured type environment: built-in scalars and generic types, registered cloneable leaves, recursive tuple types, recursive array types, and structural Fn signatures. Container shapes require no separate registration.
  • |pair: (i32, i32)| pair.0 + pair.1 declares one tuple-typed parameter, not two separate parameters.
  • The closure body is one expression.
  • A closure body resolves its own parameters and does not capture free variables from an enclosing expression. Installed library operations remain available inside it.
  • Parenthesize a closure to invoke it immediately. CEL checks the exact parameter and result shapes, including nested tuples and array element types. Arguments evaluate once, left to right; body errors propagate to the enclosing evaluation.
  • Known signatures travel through nested calls and tuple intermediates. Conditional closure alternatives must have identical known signatures. An opaque host-provided closure with no known signature reports an unsupported-call error rather than guessing a result type. A host may still supply its own call operation, which takes precedence.

Host registration

CELParser::with_type_resolver supplies annotation lookup. A registered leaf uses ValueType::cloneable_leaf::<T>() so parameters can clone owned values without requiring Copy, equality, or debug formatting. Adam supplies these descriptors through its existing TypeRegistry; no second closure-specific type registry is needed. Results move from the body into the caller and need no clone adapter of their own.

The Rust DynClosure::new, call, and call_boxed APIs remain available for existing host integrations. DynClosure::typed and DynSegment::push_closure additionally retain the complete metadata needed by general CEL invocation.

Typed callable interoperability

Fn((A, B), R) has two parameters, Fn((A,), R) has one, and Fn((), R) has none. The parameter list is always a tuple type: Fn(A, R) does not stand for one parameter. A parameter can itself be a tuple, array, or callable. Arrays compare whole signatures when checking element homogeneity.

Callable<Args, R> uses concrete canonical list arguments and an owned result. FnOf<ArgsWitness, ResultWitness> supplies its complete structural witness. Callable::from_fn binds a native function pointer; Callable::from_prepared binds a matching PreparedSegment. Both expose the same call(&args) interface with no signature comparisons during invocation. Clones share execution state; recursive entry into the same carrier returns an error. Body errors propagate. This interface prepares for a future compiled backend; that backend is not implemented.

This example checks a typed empty array and calls native and interpreted functions through the same Rust interface. Callables need no equality capability to be array elements:

#![allow(unused)]
fn main() {
/// Preserves complete callable signatures even in empty arrays, without equality.
#[test]
fn typed_callable_arrays() {
    use cel_parser::{CELParser, OpLookup};
    use cel_runtime::{
        Array, ArrayOf, CNil, CStackList, Callable, Cons, FnOf, IntoCStackList, Leaf, Nil,
    };

    type Args = Cons<Leaf<i32>, Nil>;
    type Function = FnOf<Args, ArrayOf<Leaf<i32>>>;
    let mut parser = CELParser::new(OpLookup::new());
    let empty = parser.parse_str("[]: [Fn((i32,), [i32])]").unwrap();
    let mut empty = empty.prepare::<Nil, ArrayOf<Function>>().unwrap();
    assert!(empty.call(&().into_c_stack_list()).unwrap().is_empty());

    let body = parser.parse_str("[|x: i32| [x]]").unwrap();
    let mut body = body.prepare::<Nil, ArrayOf<Function>>().unwrap();
    let interpreted = body.call(&().into_c_stack_list()).unwrap();
    let native =
        Callable::from_fn(|args: &CStackList<i32, CNil<()>>| Ok(Array::from_vec(vec![args.1])));
    for function in [native, interpreted.into_vec().pop().unwrap()] {
        assert_eq!(
            function
                .call(&(42,).into_c_stack_list())
                .unwrap()
                .as_slice(),
            [42]
        );
    }
}
}

Edge cases

  • Closure parameter lists do not accept a trailing comma.
  • (i32,) is a one-element tuple type; (i32) groups the scalar type.
  • Closure bodies do not introduce statement blocks or local binding syntax.