Working with ASTs¶
Celly treats the AST as a first-class artifact: you can inspect it, convert it to and from
the canonical cel.expr protos, turn it back into source text, and evaluate ASTs that
arrived from elsewhere.
Inspect and traverse¶
using Celly.Ast;
var ast = env.Parse("a.b + c && list.exists(x, x > threshold)").Ast!;
AstTools.ReferencedVariables(ast.Expr); // {"a", "c", "list", "threshold"} — free roots,
// comprehension variables excluded
AstTools.CalledFunctions(ast.Expr); // {"_+_", "_&&_", "_>_", ...}
AstTools.DescendantsAndSelf(ast.Expr); // pre-order walk of every node
AstTools.Children(node); // direct children of one node
Typical uses: validating that a stored policy only references allowed variables, computing which inputs to fetch before evaluation, static linting.
After a successful env.Check(ast), the AST also carries:
ast.TypeMap— every expression id → deducedCelTypeast.ReferenceMap— resolved variable names and matched overload ids per call
Proto interop (Celly.Protobuf)¶
AstConverter converts losslessly between Celly's AST and the canonical cel.expr
protos — node ids, source positions, macro-call records, types, and references all
round-trip:
using Celly.Protobuf;
using Google.Protobuf;
// Native → proto → bytes: cache or ship the compiled policy
var parsedProto = AstConverter.ToParsedExpr(ast); // cel.expr.ParsedExpr
byte[] blob = parsedProto.ToByteArray();
// Bytes → proto → native → evaluate (no re-parse needed)
var restored = AstConverter.FromParsedExpr(Cel.Expr.ParsedExpr.Parser.ParseFrom(blob));
var program = env.Program(restored);
// Checked ASTs carry the type + reference maps
env.Check(ast);
Cel.Expr.CheckedExpr checkedProto = AstConverter.ToCheckedExpr(ast);
var rehydrated = AstConverter.FromCheckedExpr(checkedProto); // IsChecked == true
Because cel.expr is the interchange format of the whole CEL ecosystem, this is the bridge
to everything else: store CheckedExpr blobs in a policy database, accept ASTs produced by
a cel-go service, or hand Celly-compiled expressions to another runtime.
Individual pieces are exposed too: AstConverter.ToProto/FromProto for bare Expr
nodes, and AstConverter.ToProtoType/TypeConverter.ToCelType for the type model.
Unparse (AST → source text)¶
using Celly.Ast;
var ast = env.Parse("[1, 2].all(x, x > 0) && has(m.f)").Ast!;
Unparser.Unparse(ast); // "[1, 2].all(x, x > 0) && has(m.f)"
- Macro expansions render in their original call form (the AST records the pre-expansion
call), so
all/exists/has/cel.bindcome back as written, not as raw comprehensions. - Parenthesization is minimal-but-correct:
1 + (2 * 3)unparses as1 + 2 * 3, while(1 + 2) * 3anda - (b - c)keep their required parens. - Non-identifier field names use backtick escaping (
m.content-type``). - Guaranteed stable:
Unparse(Parse(Unparse(x)))is a fixed point (tested).
Typical uses: policy pretty-printing, showing users a normalized form of what they wrote, generating expressions programmatically and rendering them.
Build ASTs programmatically¶
The AST node types are public with ordinary constructors (ConstExpr, CallExpr, …);
ids just need to be unique within one expression. Construct a tree, wrap it in
CelAbstractSyntax, and hand it to env.Program(...) — or unparse it to source.