Architecture
Dynamic-expressions separate what an expression is (nodes) from the way it is evaluated (visitors). A dispatcher connects these two aspects and provides hooks for caching, middleware, and serialization.
Core model
flowchart LR
dsl[Context DSL] -->|.node| nodes[Node tree]
ast[AST parser] --> nodes
pydantic[Pydantic JSON] -->|".to_node()"| nodes
nodes --> visit[dispatcher.visit]
Nodes describe the structure; visitors evaluate it. The dispatcher maps node types to visitors and runs hooks around each evaluation step.
Building expressions
There are three common ways to obtain a node tree:
| Approach | When to use |
|---|---|
| Context DSL | Rules authored in Python with type-safe field access |
| AST parser | Rules stored as strings in config or a database |
| Pydantic schemas | Rules exchanged as JSON over an API or retrieved from the database |
All paths produce the same immutable Node trees evaluated by the dispatcher.
Evaluation pipeline
Each await dispatcher.visit(node, context) creates an ExecutionContext. Every node — the root and each child visited via dispatch() — goes through the same steps shown below.
sequenceDiagram
participant Caller
participant Disp as VisitorDispatcher
participant Ext as Extension
participant MW as Middleware
participant Visitor
Caller->>Disp: visit(node, context)
loop each node
Disp->>Ext: enter on_visit(node,context,execution_context)
Note over Ext: wraps cache check, middleware, and visitor
Disp->>MW: on_visit with call_next
MW->>Visitor: visit via call_next
Visitor-->>MW: result
MW-->>Disp: result
opt child nodes
Visitor->>Disp: dispatch(child_node, context)
Note over Disp: same pipeline for each child
end
Ext-->>Disp: exit on_visit()
end
Disp-->>Caller: result
When you call await dispatcher.visit(node, context):
- Extensions — each registered
OnVisitExtensionenters an async context manager that wraps the rest of the node scope. - Per-call cache — if the node was already evaluated in this
visitcall, return the memoized result. - Middlewares — optional chain that wraps the call to
Visitor.visit. Each middleware receivescall_nextand invokes the next layer (another middleware, or the visitor at the end of the chain). - Visitor —
Visitor.visit(node, dispatch, context)evaluates the node. Composite visitors calldispatch(child, context), which re-entersVisitorDispatcher._visitfor each child with the full pipeline above. - Store result — the return value is cached in
ExecutionContextfor the remainder of the call.
Context
Context is runtime input data — a dataclass instance, a Pydantic model, or any object with attributes. FromContextNode reads fields from it during evaluation.
ExecutionContext is internal per-call state (currently a node→result cache). Extensions may read or pre-populate it.
Serialization and caching
- Serialization converts node trees to/from bytes or JSON for storage and transport.
- Cache extensions persist evaluation results in Redis (or a custom backend) across requests.
Next steps
- Getting started — minimal working example
- Nodes reference — all built-in node types
- Cookbook — end-to-end recipes