#expression-language #cel-parser #parser #cel

cel-core-parser

A CEL (Common Expression Language) parser with error recovery

3 releases

0.1.3 Jan 19, 2026
0.1.1 Jan 19, 2026
0.1.0 Jan 19, 2026

#547 in Development tools


Used in cel-core-lsp

MIT/Apache

54KB
1.5K SLoC

CEL-Core

A spec-complete Common Expression Language (CEL) implementation written in rust.

CEL-Core provides the full CEL pipeline — parsing, type checking, and evaluation — with wire compatibility to cel-go and cel-cpp through protobuf AST conversion. Built on top of the core library is a Language Server Protocol (LSP) server for IDE support.

Installation

[dependencies]
cel-core = "0.1"

# Optional: proto wire format interop with cel-go/cel-cpp
cel-core-proto = "0.1"

The CEL Environment

The Env is the entry point for working with CEL expressions. It holds variable declarations, function definitions, and extensions, then compiles expressions into type-checked ASTs and evaluable programs.

use cel_core::{Env, CelType, Value, MapActivation};

let env = Env::with_standard_library()
    .with_variable("user", CelType::String)
    .with_variable("age", CelType::Int);

let ast = env.compile("age >= 21 && user.startsWith('admin')")?;
let program = env.program(&ast)?;

let mut activation = MapActivation::new();
activation.insert("user", "admin_alice");
activation.insert("age", 25);

let result = program.eval(&activation);
assert_eq!(result, Value::Bool(true));

Compile once, evaluate many

Compilation (parsing + type checking) is the expensive step. Once you have a Program, evaluation is fast and can be called repeatedly with different variable bindings.

let ast = env.compile("age >= threshold")?;
let program = env.program(&ast)?;

for threshold in [18, 21, 65] {
    let mut activation = MapActivation::new();
    activation.insert("age", 25);
    activation.insert("threshold", threshold);
    let result = program.eval(&activation);
    println!("threshold {}: {}", threshold, result);
}

Extensions

CEL-Core implements the standard CEL extension libraries. Enable them individually or all at once:

use cel_core::ext;

// All extensions
let env = Env::with_standard_library()
    .with_all_extensions();

// Or individually
let env = Env::with_standard_library()
    .with_extension(ext::string_extension())
    .with_extension(ext::math_extension())
    .with_extension(ext::encoders_extension())
    .with_extension(ext::optionals_extension());

String extensioncharAt, indexOf, lastIndexOf, substring, split, join, strings.quote, lowerAscii, upperAscii, replace, trim, reverse, format

Math extensionmath.greatest, math.least, math.abs, math.sign, math.isNaN, math.isFinite, math.isInf, math.bitAnd, math.bitOr, math.bitNot, math.bitXor, math.bitShiftLeft, math.bitShiftRight, math.ceil, math.floor, math.round, math.trunc

Encoders extensionbase64.encode, base64.decode

Optionals extensionoptional.of, optional.none, optional.ofNonZeroValue, hasValue, value, or, orValue

Working with values

Variable bindings accept any type that implements Into<Value>. Results can be extracted back into Rust types with TryFrom:

let mut activation = MapActivation::new();
activation.insert("items", Value::list([1, 2, 3]));
activation.insert("config", Value::map([("host", "localhost"), ("port", "8080")]));

let result = program.eval(&activation);

// Extract Rust types from results
let n: i64 = (&result).try_into()?;
let s: &str = (&result).try_into()?;
let b: bool = (&result).try_into()?;
let list: &[Value] = (&result).try_into()?;
let map: &ValueMap = (&result).try_into()?;

Proto message types

CEL expressions can operate on protobuf messages. Create a ProstProtoRegistry, load your proto descriptors, and pass it to the environment:

use std::sync::Arc;
use cel_core_proto::ProstProtoRegistry;

// Create registry (includes well-known types like Timestamp, Duration, etc.)
let mut registry = ProstProtoRegistry::new();

// Load your proto definitions from a file descriptor set
// (generated by protoc --descriptor_set_out, buf build, or prost-build)
registry.add_file_descriptor_set(MY_FILE_DESCRIPTOR_SET)?;

let env = Env::with_standard_library()
    .with_proto_registry(Arc::new(registry))
    .with_variable("request", CelType::message("my.package.Request"));

Containers and abbreviations

Containers set a namespace for resolving qualified names. Abbreviations create shorthand aliases for fully-qualified type names:

let env = Env::with_standard_library()
    .with_container("my.package")
    .with_abbreviations(
        Abbreviations::from_qualified_names(&["my.package.Request"])?
    );

Error handling

CEL treats errors as values — evaluation never panics. Errors propagate through expressions following CEL's error semantics, including short-circuit behavior with logical operators:

let result = program.eval(&activation);
match &result {
    Value::Error(err) => println!("evaluation error: {}", err),
    value => println!("result: {}", value),
}

See the examples directory for more usage patterns including lists, maps, timestamps/durations, and error handling. For proto-related examples (enums, namespaces), see cel-core-proto/examples.

Proto Wire Format

The cel-core-proto crate provides bidirectional conversion between cel-core's AST and the cel-spec protobuf format. This enables wire compatibility with cel-go and cel-cpp — you can compile an expression in Rust and send the serialized AST to a Go or C++ service for evaluation, or vice versa.

use cel_core::{Env, CelType};
use cel_core_proto::AstToProto;
use prost::Message;

let env = Env::with_standard_library()
    .with_variable("x", CelType::Int);

let ast = env.compile("x + 1")?;

// Convert to cel-spec proto format
let parsed_expr = ast.to_parsed_expr();
let checked_expr = ast.to_checked_expr()?;

// Serialize to bytes (wire-compatible with cel-go/cel-cpp)
let bytes = checked_expr.encode_to_vec();

// Deserialize from bytes received from another implementation
use cel_core_proto::CheckedExpr;
let decoded = CheckedExpr::decode(bytes.as_slice())?;

Language Server

The cel-core-lsp crate provides a Language Server Protocol implementation for CEL, built with tower-lsp. It works with any LSP-compatible editor.

  • Diagnostics — Real-time parse and type checking errors as you type
  • Hover — Type information and function documentation
  • Semantic tokens — Accurate syntax highlighting
  • Protovalidate — CEL validation support in .proto files

Install

cargo install --path crates/cel-core-lsp

Conformance

CEL-Core passes 100% of the official cel-spec conformance tests across all 29 test files and all three test phases (parse, check, eval), including tests that cel-go itself currently skips. The project maintains a zero-regression policy against the conformance baseline.

For details on what is tested and how to run the conformance suite, see the conformance README.

Crate Structure

Crate Description
cel-core CEL environment, parser, type checker, evaluator, and standard library
cel-core-proto Protobuf support: proto registry (ProstProtoRegistry), message values, AST conversion for wire compatibility with cel-go/cel-cpp
cel-core-lsp Language Server Protocol implementation for IDE support
cel-core-conformance Conformance testing against the official cel-spec test suite

Development

This project uses mise for tool management and task running.

# First-time setup
mise trust && mise install
mise run conformance:setup  # initialize cel-spec submodule

# Testing
mise run test               # all tests (excludes conformance)
mise run conformance:test   # cel-spec conformance tests
mise run conformance:report # conformance report with baseline comparison

License

Licensed under either of:

at your option.

References

Dependencies

~42KB