A native-Julia evaluator for a subset of the Common Expression Language (CEL) — no wrapping of cel-go/cel-cpp/cel-c, no binary dependencies, no deps at all.
using CommonExpressionLanguage
const CEL = CommonExpressionLanguage
ctx = Dict{String,Any}(
"labels" => Dict{String,Any}("zone" => "A"),
"status" => Dict{String,Any}("battery" => 72))
CEL.evaluate_bool("labels.zone == \"A\" && status.battery > 50", ctx) # true
prog = CEL.compile("status.battery > 50") # compile once, reuse
CEL.evaluate_bool(prog, ctx; timeout=0.5) # wall-clock boundedThe practical predicate subset — boolean expressions over structured context data, the common case for policy rules, filters, and self-election predicates:
or := and ( '||' and )*
and := rel ( '&&' rel )*
rel := unary ( ('=='|'!='|'<'|'<='|'>'|'>=') unary )?
unary := '!' unary | postfix
postfix := primary ( '.' ident | '.' method '(' or ')' | '[' or ']' )*
primary := literal | ident | '(' or ')'
literal := string | number | 'true' | 'false' | 'null'
method := 'startsWith' | 'endsWith' | 'contains'
Anything outside the subset fails to compile (CELParseError), never
silently misparses — so callers can route unsupported expressions to a
fail-closed policy.
- Numeric equality/ordering is exact across Int64/UInt64/Float64 (distinct 64-bit integers never alias through a double).
boolis not numeric:true != 1.- A missing variable, member, or key is an evaluation error (CEL's
absent-field semantics), not
null. &&/||are commutative over errors: an errored arm is absorbed when the other arm alone decides the result (false decides&&, true decides||); otherwise the error propagates.- Evaluation is wall-clock bounded (
timeoutkeyword); a timeout is never absorbed by&&/||. evaluate_boolrefuses non-boolean results — no truthiness coercion.- Guards: 2048-byte source cap, 64-level nesting cap (stack-overflow proofing for hostile inputs).
The architecture mirrors the cel-spec pipeline (lexer → AST → tree-walking evaluator over a variable environment) so the rest of the language can land incrementally:
- Operators (
+ - * / %,in,? :): one entry in the parser's precedence ladder plus an_eval_binbranch. - List/map literals: two
parse_primaryproductions. - Functions and macros (
size,has,matches, comprehensions): dispatch alongside the string-method route inparse_postfix/evaluate_node. - A type checker: a separate pass over the same
Nodetree. - cel-spec conformance: the upstream conformance suite is the eventual referee for any of the above.
Contributions welcome.