Expand description
A recursive descent parser for CEL (Common Expression Language) expressions.
This crate provides a parser that can parse CEL expressions into executable segments. The parser follows the CEL grammar specification and provides detailed error reporting with source location information.
§Canonical typed execution
Arrays, tuples, unit, registered leaves, and Fn signatures compose
recursively without registering each aggregate shape. ResolvedType and
Ty retain complete shapes; shared physical carrier IDs are not identities.
Preparation validates concrete witnesses before the body executes:
use cel_parser::{CELParser, OpLookup};
use cel_runtime::{ArrayOf, Cons, IntoCStackList, IntoRustTuple, Leaf, Nil};
type Pair = Cons<Leaf<i32>, Cons<Leaf<bool>, Nil>>;
let body = CELParser::new(OpLookup::new()).parse_str("[(7, true)]").unwrap();
let mut prepared = body.prepare::<Nil, ArrayOf<Pair>>().unwrap();
let pairs = prepared.call(&().into_c_stack_list()).unwrap().into_vec();
assert_eq!(pairs.into_iter().next().unwrap().into_rust_tuple(), (7, true));Repeated prepared calls do not repeat recursive type checks. Arbitrary
erased host imports remain checked external boundaries. Cloning borrowed
argument reads, comparing values, and formatting values require their own
capabilities; merely moving a value into a container does not. Source
Fn((A,), R) names one parameter and Fn((), R) names none. The typed
Rust carrier is [cel_runtime::Callable], witnessed by [cel_runtime::FnOf].
cel-rs-macros validates source; no native macro backend is implemented.
§Error Handling
Parse errors are returned as ParseError, which carries a proc_macro2::Span for precise diagnostics.
Convert to CELError (via From) when the error must be stored or sent across thread boundaries.
All errors result from malformed input (syntax errors, type mismatches, undefined identifiers).
§Grammar
For a prose reference manual with the same grammar, see the standalone CEL book and its reference chapter.
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 { "(" [ parameter_list ] ")" | "." (unsuffixed_integer | identifier [ "(" [ parameter_list ] ")" ]) }.
primary_expression = literal | identifier [ record_fields ] | tuple_or_group | array_expression
| if_expression | closure_expression.
record_fields = "{" [ identifier ":" expression { "," identifier ":" expression } [ "," ] ] "}".
tuple_or_group = "(" [ expression ["," [ expression { "," expression } ]] ] ")".
array_expression = "[" [ expression { "," expression } ] "]" [ ":" type_expression ].
if_expression = "if" expression "{" expression "}" [ "else" ( "{" expression "}" | if_expression ) ].
closure_expression = ("||" | "|" [ closure_param { "," closure_param } ] "|") expression.
closure_param = identifier ":" type_expression.
parameter_list = expression { "," expression }.
type_expression = identifier [ "(" [ type_expression { "," type_expression } ] ")" ]
| "[" type_expression "]"
| "(" [ type_expression ["," [ type_expression { "," type_expression } ]] ] ")".
literal_pattern = ["-"] literal.Registered records use a constructor namespace independent of value bindings and evaluate
fields once in written order. All schema fields are required exactly once with exact types.
An if condition (or an embedded host header) reserves its body brace: write
if (Point { x: 0.0 }).x == 0.0 { 1 } else { 2 } to explicitly allow a record there.
Delimited argument lists, arrays, field values, and branch bodies parse ordinarily.
literal_pattern is a separate entry point, not reachable from expression — it exists for
grammars that embed CEL literals in pattern position (e.g. adam-lang’s conditional_branch)
and need Rust’s own LiteralPattern rule: a bare literal, or one directly negated by a
leading - (no !, no chained --, no arbitrary unary/postfix operand) — see
https://doc.rust-lang.org/reference/patterns.html#literal-patterns.
type_expression is the single, reusable recursive type grammar shared by array type
ascriptions ([1, 2]: [i32]), closure parameters (|x: RangeInclusive(f64)| ...), and
adam-lang cell/source/out type annotations (cell x: (i32, f64);). A bare identifier
resolves through the configured TypeResolver (array/closure contexts) or a host
TypeRegistry (adam-lang cell contexts); Name(args) names a parameterized/generic type
(e.g. RangeInclusive(f64), the type produced by CEL’s own ..= range operator) resolved
against the same TypeResolver; [T] composes nested array types ([i32], [[i32]]);
and parenthesized tuple syntax names a (possibly nested) tuple type. Every context that
accepts type_expression documents its own narrower support boundary where one exists.
CEL types recursively compose registered cloneable leaves, tuples, arrays, and callables:
arrays can contain tuples or callables, and tuples and callable signatures can contain
further compound types. Fn((A, B), R) names a callable with two parameters and result
R; use Fn((A,), R) for one parameter and Fn((), R) for none.
Adam-lang shares this recursive resolution for persistent cells, including registered
generic leaves. Adam requires cloning and value equality before storing a cell:
callable values remain valid CEL values, but a callable without equality cannot become
an Adam cell. Compound host access uses explicit canonical representation witnesses;
registered primitive and foreign leaf cells retain their concrete Rust access APIs.
§Examples
use cel_parser::{CELParser, OpLookup};
let mut segment = CELParser::new(OpLookup::new()).parse_str("10u32 + 20u32 * 5u32").unwrap();
let result = segment.call0::<u32>();
assert!(result.is_ok());
assert_eq!(result.unwrap(), 110); // 10 + 20 * 5 = 10 + 100§Basic Usage
use cel_parser::CELParser;
use cel_parser::OpLookup;
use proc_macro2::TokenStream;
use std::str::FromStr;
let input = TokenStream::from_str("10").unwrap();
let mut parser = CELParser::new(OpLookup::new());
let result = parser.parse_tokens(input.into_iter());
assert!(result.is_ok());§Receiver Operations
receiver.name and receiver.name() both invoke a unary named operation directly.
receiver.name(arg1, arg2) passes the receiver first, then the explicit arguments;
execution evaluates each operand once in source order. Value bindings named name do
not hide dot operations. Grouping changes call behavior: (receiver.name)() calls the
operation’s result through the ordinary callable lookup instead.
Host libraries supply these operations through OpLookup; the parser imposes no
domain-specific receiver types. AST parsing preserves the spelling and the member’s name
span without resolving the operation:
use cel_parser::{AstContext, Expr, OpLookup, Parser, format_expr};
let source = "a.center().x";
let expr = Parser::<AstContext>::new(OpLookup::new()).parse_str_ast(source).unwrap();
assert!(matches!(&expr, Expr::Member { args: None, .. }));
assert_eq!(format_expr(&expr, source, 0), source);§Metadata-aware static checking
ty::check_expr_with_type_resolver takes the host’s OpLookup as its fourth argument.
It checks record fields against schemas and receiver operations against signatures without
invoking emitters. Known custom leaves retain Ty::Registered descriptors; unrelated
known types never unify. Unknown constructors are diagnostic, while genuinely unresolved
values and custom calls without metadata remain best-effort. Every child is still checked,
and constructor/field/member labels never enter the value resolver. No integer-to-float
coercion occurs. ty::check_expr remains the primitive convenience entry with an empty lookup.
use cel_parser::{AstContext, OpLookup, OperationSignature, Parser, ResolvedLeafType, Ty};
use cel_runtime::ValueType;
let scalar = ResolvedLeafType::new("f64", ValueType::leaf::<f64>());
let mut lookup = OpLookup::new();
lookup.register_operation(OperationSignature {
name: "twice",
operands: vec![scalar.clone().into()],
result: scalar.into(),
emit: Box::new(|s, _| s.op1(|value: f64| value * 2.0)),
}).unwrap();
let expr = Parser::<AstContext>::new(OpLookup::new())
.parse_str_ast("3.0.twice() + true").unwrap();
let (_, diagnostics) = cel_parser::ty::check_expr_with_type_resolver(
&expr, &|_| Ty::Any, cel_parser::builtin_type_resolver().as_ref(), &lookup,
);
assert_eq!(diagnostics.len(), 1);§Array Literals
A [...] literal constructs one canonical [cel_runtime::Array] owning every element.
Checked host calls return [cel_runtime::OwnedValue]; prepared calls use exact
recursive witnesses directly.
Unannotated non-empty arrays keep their existing inference, while postfix annotations let
callers state complete recursive array, tuple, callable, and custom-leaf shapes
explicitly. Elements must share one complete recursive runtime type — a heterogeneous literal
such as [1i32, 2.0f64], an unannotated empty literal [], or a trailing comma is a parse
error — and the evaluated array converts to the corresponding Vec<T> without moving or
reallocating its elements:
use cel_parser::{CELParser, OpLookup};
let mut segment = CELParser::new(OpLookup::new())
.parse_str("[0, 1, 2]: [i32]")
.unwrap();
let array = segment.call0::<cel_runtime::OwnedValue>().unwrap().try_into_typed::<cel_runtime::ArrayOf<cel_runtime::Leaf<i32>>>().unwrap();
assert_eq!(array.into_vec(), vec![0, 1, 2]);
let mut segment = CELParser::new(OpLookup::new()).parse_str("[]: [i32]").unwrap();
let array = segment.call0::<cel_runtime::OwnedValue>().unwrap().try_into_typed::<cel_runtime::ArrayOf<cel_runtime::Leaf<i32>>>().unwrap();
assert!(array.into_vec().is_empty());Tuple arrays and callable arrays use canonical witnesses rather than external erased wrappers.
Singleton tuple types retain their trailing comma. Fn((A, B), R) has two parameters,
Fn((A,), R) has one, and Fn((), R) has none:
use cel_parser::{CELParser, OpLookup};
use cel_runtime::{ArrayOf, Cons, FnOf, IntoCStackList, Leaf, Nil};
type F = FnOf<Cons<Leaf<i32>, Nil>, ArrayOf<Leaf<i32>>>;
let body = CELParser::new(OpLookup::new())
.parse_str("[]: [Fn((i32,), [i32])]").unwrap();
let values = body.prepare::<Nil, ArrayOf<F>>().unwrap()
.call(&().into_c_stack_list()).unwrap();
assert!(values.is_empty());Hosts may also resolve custom registry-backed leaf names by constructing the parser with
CELParser::with_type_resolver:
use cel_parser::{CELParser, OpLookup, ResolvedLeafType};
use cel_runtime::ValueType;
#[derive(Clone)]
struct Custom;
let resolver = [(
"Custom",
ResolvedLeafType::new("Custom", ValueType::leaf::<Custom>()),
)];
let mut segment = CELParser::with_type_resolver(OpLookup::new(), resolver)
.parse_str("[]: [Custom]")
.unwrap();
let array = segment.call0::<cel_runtime::OwnedValue>().unwrap().try_into_typed::<cel_runtime::ArrayOf<cel_runtime::Leaf<Custom>>>().unwrap();
assert!(array.into_vec().is_empty());Nested literals accept recursive array annotations; each inner value uses the same canonical array ABI with a complete element witness:
use cel_parser::{CELParser, OpLookup};
let mut segment = CELParser::new(OpLookup::new())
.parse_str("[[0], [1]]: [[i32]]")
.unwrap();
let array = segment.call0::<cel_runtime::OwnedValue>().unwrap().try_into_typed::<cel_runtime::ArrayOf<cel_runtime::ArrayOf<cel_runtime::Leaf<i32>>>>().unwrap();
let rows = array.into_vec();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].as_slice(), &[0]);Exact annotation failures stay precise:
use cel_parser::{CELParser, OpLookup};
let err = match CELParser::new(OpLookup::new()).parse_str("[0, 1]: [f64]") {
Ok(_) => panic!("the annotation should reject i32 elements"),
Err(err) => err,
};
assert_eq!(
err.message(),
"array element 0 has type i32, expected f64"
);
let err = match CELParser::new(OpLookup::new()).parse_str("[0]: [Nope]") {
Ok(_) => panic!("the unknown leaf name should be rejected"),
Err(err) => err,
};
assert_eq!(err.message(), "unknown type `Nope`");§Known limitations
- An unannotated empty literal (
[]) is a parse error: with no element there is nothing to infer the array’s element type from (https://github.com/stlab/cel-rs/issues/212). - A type-mismatch diagnostic from the compiling path (
CELParser, which type-checks elements against their compiled runtime types) spans the whole[...]literal and names the offending element only in its message text (array element 1 has type ...). The staticty::check_exprchecker, which runs over anAstContexttree instead, already reports the offending element’s own span (https://github.com/stlab/cel-rs/issues/215).
§Error Formatting
use annotate_snippets::Renderer;
use cel_parser::CELParser;
use cel_parser::OpLookup;
use proc_macro2::TokenStream;
use std::str::FromStr;
let line = line!() + 1;
let source = r#"
10 20
"#; // Invalid: missing operator
let input = TokenStream::from_str(source).unwrap();
let mut parser = CELParser::new(OpLookup::new());
if let Err(e) = parser.parse_tokens(input.into_iter()) {
// Format error starting at line 1
println!("{}", e.format_rustc_style(source, file!(), line, &Renderer::plain()));
}Re-exports§
pub use registered_ops::OperationEmitter;pub use registered_ops::OperationSignature;pub use registered_ops::RecordEmitter;pub use registered_ops::RecordField;pub use registered_ops::RecordSchema;pub use ast::AstContext;pub use ast::ClosureParam;pub use ast::Expr;pub use ast::ExprSpan;pub use ast::Literal;pub use ast::LogicalOp;pub use ast::RecordInit;pub use op_table::OpLookup;pub use op_table::OperandTypes;pub use op_table::builtin_operand_types;pub use parser_context::DynSegmentContext;pub use parser_context::ParserContext;pub use trivia::Comment;pub use ty::Ty;pub use type_expr::ResolvedArrayType;pub use type_expr::ResolvedLeafType;pub use type_expr::ResolvedType;pub use type_expr::TypeExpr;pub use type_expr::TypeResolver;pub use type_expr::builtin_type_resolver;pub use type_expr::resolve_builtin_named_type;
Modules§
- ast
- A span-carrying CEL expression AST, built by
AstContextas an alternative toDynSegmentContext’s direct execution. Consumed as-is by adam-lang (method bodies/initializers), the language server, the formatter, and the future macro-compilation backend. Carries no resolved types or operator overloads: resolution and type/range validation are deferred to a later, separate phase. - lex_
lexer - Flattens Rust token trees into CEL tokens while preserving source spans.
- op_
table - Operation table for dynamically dispatching operations based on type signatures.
- parser_
context ParserContext: the pluggable target a CEL grammar production emits into.- registered_
ops - Persistent operation and record metadata shared by independent host libraries.
- trivia
- Shared trivia recovery: turns the raw source text of a grammatically-constrained gap
(whitespace, comments, and a known fixed sequence of literal tokens) into an ordered sequence
of
GapPieces, and convertsproc_macro2span positions to byte offsets. Used bycel-parser’s own expression formatter and byadam-lang’s declaration formatter, so both recover comments the same way. See https://github.com/stlab/cel-rs/issues/201. - ty
- Checks source expressions using complete recursive array, tuple, and callable shapes.
- type_
expr - Recursive CEL type expressions plus the small resolver boundary they need.
Structs§
- CELError
- A CEL parse error with a message and source location.
- Line
Column - A line-column pair representing the start or end of a
Span. - Parse
Error - A parse error carrying the original
proc_macro2::Spanof the offending token or expression start, plus an optional end span for range errors. - Parser
- A recursive descent parser for expressions, generic over the
ParserContextit emits into. - Source
Span - Source region as start/end line and column.
- Span
Context - A runtime error context carrying the source span of the failing operation.
- Span
Label - One labelled span in a multi-span diagnostic.
Enums§
- Expression
Restriction - Selects whether a header reserves an identifier’s following brace for its body.
Traits§
- Format
Rustc Style - Extension trait that adds rustc-style formatting to
anyhow::Error.
Functions§
- format_
expr - Pretty-prints
exprback to CEL source text — see the module doc for the printing rules. - format_
expr_ header - Formats a brace-reserving header and its surrounding comments within explicit boundaries.
- format_
expr_ header_ with_ layout - Formats a brace-reserving header at
column, reservingsuffix_columnson its last line. - format_
expr_ restricted - Formats an embedded expression while preserving a header’s brace reservation.
- format_
expr_ with_ layout - Formats an expression at the actual output
columnwith a targetwidth. - format_
multi_ span - Renders
titlewith several labelled annotations oversource_code, the first as the primary caret and the rest as secondary context, in rustc style. - validate_
literal - Validates that
litis a literalcel_parsercan represent as a value: a recognized numeric suffix, and (for a suffixed or unsuffixed integer/float) a value in range for its width. Reuses the same literal-compiling checks the grammar itself applies to a literal token, against a throwaway context, for a caller (e.g. adam-lang’s CST parser, which records a literal token without compiling it into a value) that needs to reject the same literals the runtime parser would, without needing aParserContextof its own to push into.