Skip to main content

Crate cel_parser

Crate cel_parser 

Source
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 static ty::check_expr checker, which runs over an AstContext tree 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 AstContext as an alternative to DynSegmentContext’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 converts proc_macro2 span positions to byte offsets. Used by cel-parser’s own expression formatter and by adam-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.
LineColumn
A line-column pair representing the start or end of a Span.
ParseError
A parse error carrying the original proc_macro2::Span of the offending token or expression start, plus an optional end span for range errors.
Parser
A recursive descent parser for expressions, generic over the ParserContext it emits into.
SourceSpan
Source region as start/end line and column.
SpanContext
A runtime error context carrying the source span of the failing operation.
SpanLabel
One labelled span in a multi-span diagnostic.

Enums§

ExpressionRestriction
Selects whether a header reserves an identifier’s following brace for its body.

Traits§

FormatRustcStyle
Extension trait that adds rustc-style formatting to anyhow::Error.

Functions§

format_expr
Pretty-prints expr back 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, reserving suffix_columns on 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 column with a target width.
format_multi_span
Renders title with several labelled annotations over source_code, the first as the primary caret and the rest as secondary context, in rustc style.
validate_literal
Validates that lit is a literal cel_parser can 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 a ParserContext of its own to push into.

Type Aliases§

CELParser
A recursive descent parser that executes directly into a [DynSegment].
Result
Parser result type.