Module

compiler.partial_eval

Compile-time partial evaluation of template AST.

Transforms a Kida AST by evaluating expressions whose values can be determined from a static context (values known at compile time, not at render time). This replaces dynamic lookups with constant data nodes, enabling more aggressive f-string coalescing and producing templates where static regions are literal strings in bytecode.

Example:

Given static_context = {"site": Site(title="My Blog")}:

Before:  Output(expr=Getattr(Name("site"), "title"))
After:   Data(value="My Blog")

The evaluator is conservative: if any sub-expression cannot be resolved, the entire expression is left untouched. This guarantees that partial evaluation never changes observable behavior.

Integration:

Called by ``Environment._compile()`` between parsing and compilation.
The partially-evaluated AST is then compiled normally by the Compiler,
which sees more Data/Const nodes and produces better coalesced output.

Classes

_LoopProperties 7
Compile-time stand-in for LoopContext properties. Provides the same attribute names as the runtime…

Compile-time stand-in for LoopContext properties.

Provides the same attribute names as the runtime LoopContext so that expressions like{{ loop.index }}resolve during partial evaluation.

Attributes

Name Type Description
index0 int
index int
first bool
last bool
length int
revindex int
revindex0 int
PartialEvaluator 33
Evaluate static expressions in a Kida AST at compile time. Walks the template AST and replaces exp…

Evaluate static expressions in a Kida AST at compile time.

Walks the template AST and replaces expressions that can be fully resolved from the static context with their computed values.

Methods

evaluate 1 Template
Transform a template AST by evaluating static expressions. Returns a new Templ…
def evaluate(self, template: Template) -> Template

Transform a template AST by evaluating static expressions.

Returns a new Template node with static parts replaced by constants. The original template is not modified.

Parameters
Name Type Description
template
Returns
Template
Internal Methods 32
__init__ 6
def __init__(self, static_context: dict[str, Any], *, escape_func: Any | None = None, pure_filters: frozenset[str] = frozenset(), filter_callables: dict[str, Callable[..., Any]] | None = None, max_eval_depth: int = 100, inline_components: bool = False) -> None
Parameters
Name Type Description
static_context
escape_func Default:None
pure_filters Default:frozenset()
filter_callables Default:None
max_eval_depth Default:100
inline_components Default:False
_try_eval 2 Any
Try to evaluate an expression against the static context. Returns the computed…
def _try_eval(self, expr: Expr, depth: int = 0) -> Any

Try to evaluate an expression against the static context.

Returns the computed value on success, or_UNRESOLVEDif the expression depends on runtime values.

Depth limit prevents stack overflow from deeply nested attribute chains (e.g. a.b.c.d.e... with 500+ levels).

Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_try_eval_filter 2 Any
Evaluate a Filter node when value and args are resolvable. OptionalFilter (``?…
def _try_eval_filter(self, expr: Filter, depth: int = 0) -> Any

Evaluate a Filter node when value and args are resolvable.

OptionalFilter (?|) returns None when the input value is None.

Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_try_eval_pipeline 2 Any
Evaluate a Pipeline node when value and all steps are resolvable. SafePipeline…
def _try_eval_pipeline(self, expr: Pipeline, depth: int = 0) -> Any

Evaluate a Pipeline node when value and all steps are resolvable.

SafePipeline (?|>) propagates None through the chain.

Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_try_eval_listcomp 2 Any
Evaluate a list comprehension when iterable and all parts resolve.
def _try_eval_listcomp(self, expr: ListComp, depth: int = 0) -> Any
Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_try_eval_funccall 2 Any
Evaluate a FuncCall when it targets a safe builtin and all args resolve.
def _try_eval_funccall(self, expr: FuncCall, depth: int = 0) -> Any
Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_try_eval_range 2 Any
Evaluate a Range literal (start..end or start...end).
def _try_eval_range(self, expr: Range, depth: int = 0) -> Any
Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_try_eval_test 2 Any
Evaluate a Test node when the value can be resolved. Handles ``is defined`` / …
def _try_eval_test(self, expr: Test, depth: int = 0) -> Any

Evaluate a Test node when the value can be resolved.

Handlesis defined / is undefinedspecially (they check context membership, not the value itself). All other tests require a resolved value.

Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_eval_binop 3 Any
Evaluate a binary operation with known operands.
staticmethod
def _eval_binop(op: str, left: Any, right: Any) -> Any
Parameters
Name Type Description
op
left
right
Returns
Any
_eval_unaryop 2 Any
Evaluate a unary operation with a known operand.
staticmethod
def _eval_unaryop(op: str, operand: Any) -> Any
Parameters
Name Type Description
op
operand
Returns
Any
_eval_compare 2 Any
Evaluate a comparison chain with known operands.
def _eval_compare(self, expr: Compare, depth: int = 0) -> Any
Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_eval_boolop 2 Any
Evaluate a boolean operation with short-circuit semantics.
def _eval_boolop(self, expr: BoolOp, depth: int = 0) -> Any
Parameters
Name Type Description
expr
depth Default:0
Returns
Any
_transform_body 1 Sequence[Node]
Transform a sequence of nodes, merging adjacent Data nodes.
def _transform_body(self, body: Sequence[Node]) -> Sequence[Node]
Parameters
Name Type Description
body
Returns
Sequence[Node]
_append_and_merge 2
Append a node, merging adjacent Data nodes.
staticmethod
def _append_and_merge(nodes: list[Node], new_node: Node) -> None
Parameters
Name Type Description
nodes
new_node
_transform_node 1 Node | None
Transform a single AST node. Returns the transformed node, or None to remove i…
def _transform_node(self, node: Node) -> Node | None

Transform a single AST node.

Returns the transformed node, or None to remove it.

Parameters
Name Type Description
node
Returns
Node | None
_transform_output 1 Node
Try to evaluate an Output node to a Data node.
def _transform_output(self, node: Output) -> Node
Parameters
Name Type Description
node
Returns
Node
_transform_if 1 Node | None
Try to evaluate an If node's test at compile time.
def _transform_if(self, node: If) -> Node | None
Parameters
Name Type Description
node
Returns
Node | None
_transform_for 1 Node | None
Unroll for-loop when iterable is statically known, else recurse.
def _transform_for(self, node: For) -> Node | None
Parameters
Name Type Description
node
Returns
Node | None
_transform_with 1 Node
Propagate static values through {% with %} block bindings. Evaluates each bind…
def _transform_with(self, node: With) -> Node

Propagate static values through {% with %} block bindings.

Evaluates each binding expression against the static context. Resolved bindings are added to a sub-evaluator's context so that the body can fold expressions referencing them.

Parameters
Name Type Description
node
Returns
Node
_transform_match 1 Node | None
Eliminate dead match/case branches when subject is compile-time-known. When th…
def _transform_match(self, node: Match) -> Node | None

Eliminate dead match/case branches when subject is compile-time-known.

When the subject resolves, iterate cases and match:

  • Const patterns: exact equality check
  • Name("_") wildcard: always matches
  • Other patterns: bail (leave Match intact)

If subject is unresolved, recurse into each case body.

Parameters
Name Type Description
node
Returns
Node | None
_try_unroll_for 2 Node | None
Attempt to unroll a for-loop with a known iterable. Returns an _InlinedBody of…
def _try_unroll_for(self, node: For, iter_val: Any) -> Node | None

Attempt to unroll a for-loop with a known iterable.

Returns an _InlinedBody of unrolled iterations, or None if unrolling is not possible (too many items, complex target, etc.).

Parameters
Name Type Description
node
iter_val
Returns
Node | None
_build_iter_context 2 dict[str, Any]
Build a sub-context mapping target variable(s) to the current item.
def _build_iter_context(self, target_names: tuple[str, ...], item: Any) -> dict[str, Any]
Parameters
Name Type Description
target_names
item
Returns
dict[str, Any]
_make_sub_evaluator 1 PartialEvaluator
Create a sub-evaluator with a merged context.
def _make_sub_evaluator(self, ctx: dict[str, Any]) -> PartialEvaluator
Parameters
Name Type Description
ctx
Returns
PartialEvaluator
_transform_assignment 1 Node
Track Set/Let bindings so downstream expressions resolve. When the assigned va…
def _transform_assignment(self, node: Set | Let) -> Node

Track Set/Let bindings so downstream expressions resolve.

When the assigned value can be fully evaluated from the static context, add it to self._ctx so subsequent expressions referencing this variable are also foldable. The value expression is also replaced with a Const so the runtime assignment doesn't fail looking up now-unnecessary vars.

Parameters
Name Type Description
node
Returns
Node
_transform_export 1 Node
Partially transform Export value expression. Export has the same structure as …
def _transform_export(self, node: Export) -> Node

Partially transform Export value expression.

Export has the same structure as Let (name + value), but with export-from-scope semantics. We need to partially transform the value expression so loop variable references are replaced with constants when the enclosing for-loop is unrolled.

Parameters
Name Type Description
node
Returns
Node
_transform_capture 1 Node
Recurse into Capture body so expressions are partially transformed. Without th…
def _transform_capture(self, node: Capture) -> Node

Recurse into Capture body so expressions are partially transformed.

Without this, a Capture inside an unrolled for-loop would retain references to the (now-removed) loop variable.

Parameters
Name Type Description
node
Returns
Node
_try_inline_call 1 Node | None
Try to inline a CallBlock by expanding its Def body. Returns an _InlinedBody (…
def _try_inline_call(self, node: CallBlock) -> Node | None

Try to inline a CallBlock by expanding its Def body.

Returns an _InlinedBody (or single node) on success, None if inlining is not possible.

Requirements for inlining:

  • The call target is a simple FuncCall(Name(def_name))
  • The referenced Def has been seen in this template
  • The Def body is small (< _MAX_INLINE_NODES)
  • The Def has no Slot nodes (no caller injection)
  • No slot content in the CallBlock
  • All arguments resolve to constants
Parameters
Name Type Description
node
Returns
Node | None
_count_nodes 1 int
Count total nodes in a body (shallow — does not recurse into children).
staticmethod
def _count_nodes(body: Sequence[Node]) -> int
Parameters
Name Type Description
body
Returns
int
_has_slot 1 bool
Check if body contains any Slot nodes (recursively).
staticmethod
def _has_slot(body: Sequence[Node]) -> bool
Parameters
Name Type Description
body
Returns
bool
_transform_expr 1 Expr
Try to partially evaluate an expression. If sub-expressions can be resolved, r…
def _transform_expr(self, expr: Expr) -> Expr

Try to partially evaluate an expression.

If sub-expressions can be resolved, replace them with Const nodes.

Parameters
Name Type Description
expr
Returns
Expr
_transform_boolop 1 Expr
Partially simplify BoolOp when some operands are statically known. Template ex…
def _transform_boolop(self, expr: BoolOp) -> Expr

Partially simplify BoolOp when some operands are statically known.

Template expressions have no side effects, so short-circuiting is always safe:

  • false and XFalse
  • true or XTrue
  • Mixed: filter out resolved non-terminating operands.
Parameters
Name Type Description
expr
Returns
Expr
_transform_condexpr 1 Expr
Collapse CondExpr when the test resolves statically.
def _transform_condexpr(self, expr: CondExpr) -> Expr
Parameters
Name Type Description
expr
Returns
Expr
_InlinedBody 1
Temporary node for inlined If branches. Holds multiple nodes that replace a single If node. The p…

Temporary node for inlined If branches.

Holds multiple nodes that replace a single If node. The parent's _transform_bodyflattens these into the body sequence.

Attributes

Name Type Description
nodes Sequence[Node]

Functions

_try_eval_const_only 1 Any
Evaluate expression using only literals and constant expressions. Resolves Con…
def _try_eval_const_only(expr: Expr) -> Any

Evaluate expression using only literals and constant expressions.

Resolves Const, BinOp, UnaryOp, Compare, BoolOp. No Name/Getattr/Getitem (those require static_context). Used for dead code elimination.

Parameters
Name Type Description
expr Expr
Returns
Any
_body_has_scoping_nodes 1 bool
True if body contains Set, Let, Capture, or Export (block-scoped).
def _body_has_scoping_nodes(nodes: Sequence[Node]) -> bool
Parameters
Name Type Description
nodes Sequence[Node]
Returns
bool
_dce_transform_if 1 Node | None
Eliminate dead branches when test is const-only resolvable.
def _dce_transform_if(node: If) -> Node | None
Parameters
Name Type Description
node If
Returns
Node | None
_dce_transform_match 1 Node | None
Eliminate dead match/case branches when subject is a const literal.
def _dce_transform_match(node: Match) -> Node | None
Parameters
Name Type Description
node Match
Returns
Node | None
_dce_transform_body 1 Sequence[Node]
Transform body, eliminating dead If nodes and flattening _InlinedBody.
def _dce_transform_body(body: Sequence[Node]) -> Sequence[Node]
Parameters
Name Type Description
body Sequence[Node]
Returns
Sequence[Node]
eliminate_dead_code 1 Template
Remove branches whose conditions are provably constant. Runs without static_co…
def eliminate_dead_code(template: Template) -> Template

Remove branches whose conditions are provably constant.

Runs without static_context. Eliminates e.g. {% if false %}...{% end %}, {% if true %}x{% else %}y{% end %}, {% if 1+1==2 %}...{% end %}.

Parameters
Name Type Description
template Template
Returns
Template
_compare_op 3 bool
Evaluate a comparison operator.
def _compare_op(op: str, left: Any, right: Any) -> bool
Parameters
Name Type Description
op str
left Any
right Any
Returns
bool
partial_evaluate 6 Template
Convenience function: partially evaluate a template AST.
def partial_evaluate(template: Template, static_context: dict[str, Any], *, escape_func: Any | None = None, pure_filters: frozenset[str] = frozenset(), filter_callables: dict[str, Callable[..., Any]] | None = None, inline_components: bool = False) -> Template
Parameters
Name Type Description
template Template

Parsed template AST.

static_context dict[str, Any]

Values known at compile time.

escape_func Any | None

HTML escape function for static Output nodes.

Default:None
pure_filters frozenset[str]

Filter names safe for compile-time evaluation.

Default:frozenset()
filter_callables dict[str, Callable[..., Any]] | None

Filter name to callable for Filter/Pipeline eval.

Default:None
inline_components bool

When True, small {% def %} calls with all-constant arguments are expanded inline at compile time.

Default:False
Returns
Template
_flatten_inlined 1 Template
Flatten _InlinedBody nodes from branch elimination.
def _flatten_inlined(template: Template) -> Template
Parameters
Name Type Description
template Template
Returns
Template
_flatten_body 1 Sequence[Node]
Flatten any _InlinedBody nodes in a body sequence.
def _flatten_body(body: Sequence[Node]) -> Sequence[Node]
Parameters
Name Type Description
body Sequence[Node]
Returns
Sequence[Node]