Architecture Overview¶
This section is a guided tour of how Celly actually works, written to be read in order. Every stage links to the source file that implements it.
The pipeline¶
A CEL expression goes through four stages:
flowchart LR
SRC["source text<br/>x + 1 > 3"] --> LEX[Lexer]
LEX -->|tokens| PARSE[Parser<br/>+ macro expansion]
PARSE -->|AST| CHECK[Type Checker<br/><i>optional</i>]
CHECK -->|typed AST| PLAN[Planner]
PARSE -.->|unchecked AST| PLAN
PLAN -->|IInterpretable tree| EVAL[Eval + Activation]
EVAL --> VAL["CelValue"]
| Stage | Source | Job |
|---|---|---|
| Lexer | src/Celly/Parsing/Lexer.cs |
text → tokens (strings, numbers, operators) |
| Parser | src/Celly/Parsing/Parser.cs |
tokens → AST; expands macros as it goes |
| Checker | src/Celly/Checking/Checker.cs |
annotates every AST node with a type; rejects bad programs |
| Planner | src/Celly/Interpreter/Planner.cs |
AST → tree of IInterpretable objects |
| Eval | src/Celly/Interpreter/Interpretables.cs |
walks the interpretable tree against an activation |
Two properties shape everything:
- The checker is optional (gradual typing). Evaluation must work on unchecked ASTs
where every type is
dyn— so the runtime carries full type information in its values and does its own dispatch. - Errors are values, not exceptions.
ErrorValue/UnknownValueflow through evaluation, which is what makes CEL's commutative error absorption expressible.
The cast of data structures¶
flowchart TB
subgraph "compile time"
AST["Expr AST<br/>ConstExpr / IdentExpr / SelectExpr /<br/>CallExpr / ListExpr / MapExpr /<br/>StructExpr / ComprehensionExpr"]
TYPE["CelType<br/>one type model shared by<br/>checker AND runtime"]
end
subgraph "run time"
IV["IInterpretable tree<br/>one node type per behavior"]
CV["CelValue hierarchy<br/>IntValue, StringValue, ListValue, …<br/>+ ErrorValue, UnknownValue, OptionalValue"]
end
AST --> IV
TYPE --- CV
Expr(src/Celly/Ast/Expr.cs) mirrors the canonicalcel.expr.Exprproto shape — eight node kinds, each with a parse-unique id used by source positions and type maps.CelType(src/Celly/Types/CelType.cs) is a single type model used both by the checker (parameterized:list(int), type params) and at runtime (type(x)yields one). Primitives are singletons sotype(1) == intis structural.CelValue(src/Celly/Values/) is the runtime value hierarchy; capability interfaces (IAdder,IComparableValue,IIndexerValue, …) drive operator dispatch.IInterpretableis the compiled form:Eval(IActivation) → CelValue, one class per behavior (constant, ident lookup, call, logical-and, comprehension, …).
The extension seams¶
Two seams keep the core dependency-free while letting features plug in:
flowchart LR
subgraph CORE["Celly core (zero deps)"]
ENV[CelEnv] --> P[Planner]
ENV --> C[Checker]
P & C --> SEAM(["ITypeProvider / ITypeAdapter"])
ENV --> LIB(["CelLibrary"])
end
PROTO["Celly.Protobuf<br/>ProtoTypeRegistry"] -. implements .-> SEAM
EXT["Extensions<br/>strings, math, optionals, …"] -. registers into .-> LIB
ITypeProvider/ITypeAdapter(src/Celly/Providers/): struct construction, field types, enum constants, native-value adaption.Celly.Protobuf'sProtoTypeRegistryimplements both; the core ships aNativeTypeAdapterfor plain .NET values. This is the only door protobuf enters through.CelLibrary(src/Celly/CelLibrary.cs): an extension = macros (parse time) + function implementations (run time) + declarations (check time), registered as one unit. All nine extension libraries — and even the strong-enum mode — are justCelLibraryinstances.
Reading order¶
- Lexer & Parser — how text becomes an AST without a parser generator
- Macros — how
all()/exists()become loops without loops - The Value Model — runtime values and the sharp edges of CEL numerics
- Type Checker — unification, gradual typing, name resolution
- Planner & Evaluator — how evaluation actually executes
- Protobuf Integration — messages, well-known types, Any
- Conformance Testing — how 100% is measured and kept