A native-Julia evaluator for a subset of the
Common Expression Language (CEL) — no wrapping of
cel-go/cel-cpp/cel-c, and no dependencies beyond the libpcre2 that
ships inside every Julia (the PCRE2_jll stdlib, used by matches).
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:
expr := or ( '?' or ':' expr )?
or := and ( '||' and )*
and := rel ( '&&' rel )*
rel := add ( ('=='|'!='|'<'|'<='|'>'|'>='|'in') add )?
add := mul ( ('+'|'-') mul )*
mul := unary ( ('*'|'/'|'%') unary )*
unary := ('!'|'-') unary | postfix
postfix := primary ( '.' ident | '.' method '(' expr ')'
| '.' macro '(' ident ',' expr ')' | '[' expr ']' )*
primary := literal | ident | func '(' expr (',' expr)* ')'
| '(' expr ')' | '[' exprlist? ']' | '{' entrylist? '}'
literal := string | number | 'true' | 'false' | 'null'
method := 'startsWith' | 'endsWith' | 'contains' | 'matches'
macro := 'exists' | 'all' | 'exists_one' | 'filter' | 'map'
func := 'size' | 'has' | 'string' | 'int' | 'uint' | 'double'
| 'bool' | 'dyn' | 'matches'
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.- Arithmetic is checked (cel-spec): Int64/UInt64 overflow, integer
division/modulo by zero, and
INT64_MIN / -1are evaluation errors, never wraparound; integer/truncates toward zero and%keeps the dividend's sign; doubles follow IEEE 754 (x / 0.0is ±Inf, and%has no double overload). Operands must share a numeric type — cel-spec has no implicit promotion (1 + 1uerrors) even though equality is exact across types.+also concatenates strings and lists. - A missing variable, member, or key is an evaluation error (CEL's
absent-field semantics), not
null;has(e.f)is the presence test and, being a macro, only compiles on a field selection. - Map literal keys are string/int/uint/bool; a duplicate key is an
evaluation error; map indexing and
inuse the same exact cross-type equality as==. - Comprehension macros follow cel-spec error absorption:
existsabsorbs per-element errors once atrueis found,allonce afalseis found;exists_one/filter/mappropagate the first error. Map comprehensions iterate keys. - The ternary evaluates only the taken branch and requires a
boolcondition. matchesruns on libpcre2's non-backtracking DFA matcher (pcre2_dfa_match, the same algorithm family as RE2), via the PCRE2_jll stdlib that ships inside every Julia. cel-spec pinsmatches()to RE2 syntax and to a polynomial cost bound (part of CEL's terminating guarantee); the DFA meets the cost bound, and a compile-time screen enforces the syntax: a PCRE-only construct (backreference, lookaround, atomic/possessive group,\Z, …) is a fail-closedCELEvalErrorexactly as under RE2 — crucial because the DFA matcher would otherwise silently match lookarounds. The screen also rewrites the escapes whose meaning differs (\v,\s,\S, octal, non-multiline$,(?m)^) to spellings with the RE2 meaning, a mapping fuzz-verified against the real C++ RE2 (hundreds of thousands of pattern/subject checks, zero disagreements; the one accepted divergence is\b/\Binside a multi-byte character, where C++ RE2's byte-level automaton can assert but a codepoint-level matcher cannot). Matching cannot catastrophically backtrack — a hostile pattern or input cannot become a denial of service.&&/||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), re-checked every comprehension iteration; a timeout is never absorbed by&&/||or byexists/all. 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:
- bytes/timestamp/duration values and their functions.
type(x)and type values: deliberately skipped so far — honest type values need type literals likeintas identifiers, and a string stand-in would maketype(x) == "int"hold where cel-spec says it must not.- 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.