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

Literals and types

CEL source includes literal values for numbers, booleans, text, characters, and bytes. It also uses tuple, array, range, and closure syntax to build larger values from expressions. For the full grammar of these forms, see the Reference Manual.

Built-in scalar type names

These sixteen names are the built-in scalar type names used by as casts and typed closure parameters.

CategoryTypes
Signed integersi8, i16, i32, i64, i128, isize
Unsigned integersu8, u16, u32, u64, u128, usize
Floating-point numbersf32, f64
Other scalarsbool, String

The language also accepts character, byte-string, C-string, and unit values in expressions. The named scalar type set remains the sixteen type names listed above.

Syntax

literal = integer_literal
        | floating_point_literal
        | boolean_literal
        | string_literal
        | character_literal
        | byte_literal
        | byte_string_literal
        | c_string_literal .

Integer literals

An integer literal is a whole number optionally followed by one of the accepted integer suffixes.

SuffixResulting type
nonei32
i8i8
i16i16
i32i32
i64i64
i128i128
isizeisize
u8u8
u16u16
u32u32
u64u64
u128u128
usizeusize

Unary - combines with a numeric literal to form a negative numeric expression.

0
42i8
42i64
42u16
42u128
42usize
-1i32

Floating-point literals

A floating-point literal uses decimal or exponent notation, optionally followed by a floating-point suffix.

SuffixResulting type
nonef64
f64f64
f32f32
3.5
1e3
0.25f64
6.022e23f32
-3.5

Boolean, string, character, byte, and unit values

Boolean literals are true and false. String literals produce String values. Character literals produce char values. Byte literals produce u8 values. Byte-string and C-string literals produce byte-oriented string values. Write the unit value as ().

true
false
"hello"
'a'
b'A'
b"bytes"
c"header"
()

A byte literal such as b'A' and an explicitly suffixed integer such as 65u8 both denote u8 values.

Compound values

CEL also builds compound values from ordinary expressions:

  • () is the unit value, (expr) is grouping, (expr,) is a 1-tuple, and (a, b, c) is a tuple.
  • Arrays are homogeneous bracketed lists. An unannotated array is non-empty; a type annotation such as []: [i32] supplies the element type for an empty array.
  • || expr and |x: T| expr are closure values.
  • These forms can nest inside calls, tuples, arrays, conditionals, and one another wherever the grammar permits.
  • Array homogeneity compares complete recursive shapes, including every tuple field and callable parameter/result. []: [(i32, [f64])] and []: [Fn((i32,), [i32])] need no registration of those aggregate shapes.
(1,)
(1, 2, 3)
[0, 1, 2]
[0, 1]: [i32]
[]: [i32]
|x: i32| x + 1

Source () and the empty tuple type are one canonical type, witnessed by Nil and represented by CNil<()>. Bridge ordinary Rust () with the zero-element IntoCStackList / IntoRustTuple conversion; it is not a separate source leaf that merely shares a zero-sized layout. An explicitly registered opaque host Leaf<()> remains a distinct host identity.

#![allow(unused)]
fn main() {
/// Normalizes source unit to the zero-element list and bridges it to Rust unit.
#[test]
fn source_unit_is_an_empty_list() {
    use cel_parser::{CELParser, OpLookup};
    use cel_runtime::{CNil, IntoCStackList, IntoRustTuple, Nil};

    let body = CELParser::new(OpLookup::new()).parse_str("()").unwrap();
    let mut body = body.prepare::<Nil, Nil>().unwrap();
    let unit: CNil<()> = body.call(&().into_c_stack_list()).unwrap();
    let (): () = unit.into_rust_tuple();
}
}

Host-registered records

A host can register a named record schema and operations without changing CEL. Write Sample { value: 3.0, } to construct a record when the host supplies that schema. Field labels select schema fields rather than variables; values evaluate in written order. Supply every field exactly once. Empty schemas accept {}, and non-empty field lists accept a trailing comma. CEL reports unknown constructors, unknown/duplicate/missing fields, and mismatched field types.

Records use exact scalar types: 1 is i32, never implicitly f64; use 1.0 or an explicit supported cast for an f64 field. Constructor names occupy a separate namespace from values, so a value named Sample does not hide the constructor. The host must separately register any operation such as .value; a record schema does not automatically create accessors.

This executable embedding example registers a real Rust value and accessor. Both bare and parenthesized member spellings return 3.0; calling that numeric result with (record.value)() fails.

#![allow(unused)]
fn main() {
/// Evaluates a host record and its direct accessor with exact field types.
#[test]
fn registered_record_and_member_example() {
    use cel_parser::{OperationSignature, RecordField, RecordSchema, ResolvedLeafType};
    use cel_runtime::ValueType;

    /// A host-defined value, not a built-in CEL type.
    #[derive(Clone, Debug, PartialEq)]
    struct Sample {
        value: f64,
    }
    let sample = ResolvedLeafType::new("Sample", ValueType::leaf::<Sample>());
    let scalar = ResolvedLeafType::new("f64", ValueType::leaf::<f64>());
    let mut lookup = OpLookup::new();
    lookup
        .register_record(RecordSchema {
            ty: sample.clone(),
            fields: vec![RecordField {
                name: "value",
                ty: scalar.clone().into(),
            }],
            emit: Box::new(|s, _, _, _| s.op1(|value: f64| Sample { value })),
        })
        .unwrap();
    lookup
        .register_operation(OperationSignature {
            name: "value",
            operands: vec![sample.into()],
            result: scalar.into(),
            emit: Box::new(|s, _| s.op1(|sample: Sample| sample.value)),
        })
        .unwrap();
    let mut parser = CELParser::new(lookup);
    for source in [
        "Sample { value: 3.0, }.value",
        "Sample { value: 3.0 }.value()",
    ] {
        assert_eq!(
            parser.parse_str(source).unwrap().call0::<f64>().unwrap(),
            3.0
        );
    }
    assert_eq!(
        parser
            .parse_str("if (Sample { value: 3.0 }.value) > 0.0 { 1 } else { 0 }")
            .unwrap()
            .call0::<i32>()
            .unwrap(),
        1
    );
    assert!(parser.parse_str("Sample { value: 1 }").is_err());
    assert!(parser.parse_str("(Sample { value: 3.0 }.value)()").is_err());
    assert!(
        parser
            .parse_str("Sample { value: 3.0 }.value = 4.0")
            .is_err()
    );
}
}

Range values

A range expression produces a range value from zero, one, or two numeric endpoints. CEL supports bounded, half-bounded, and unbounded forms:

a..b
a..=b
a..
..b
..=b
..

Endpoint-bearing ranges require homogeneous numeric endpoints. Each endpoint is a full expression, so operators inside either side are parsed before the range is formed. The Operators chapter defines precedence and the non-chaining rule for range expressions.

Use this chapter for value categories and Reference Manual for the complete grammar of literal, tuple, array, range, and closure syntax.