# partial_eval

URL: /kida/api/compiler/partial_eval/
Section: compiler
Description: 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.

---

> For a complete page index, fetch /kida/llms.txt.

Open LLM text
(/kida/api/compiler/partial_eval/index.txt)

Share with AI

Ask Claude
(https://claude.ai/new?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fkida%2Fapi%2Fcompiler%2Fpartial_eval%2Findex.txt)

Ask ChatGPT
(https://chatgpt.com/?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fkida%2Fapi%2Fcompiler%2Fpartial_eval%2Findex.txt)

Ask Gemini
(https://gemini.google.com/app?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fkida%2Fapi%2Fcompiler%2Fpartial_eval%2Findex.txt)

Ask Copilot
(https://copilot.microsoft.com/?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fkida%2Fapi%2Fcompiler%2Fpartial_eval%2Findex.txt)

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.
```

3Classes10Functions

## 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`_UNRESOLVED`if 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.

Handles`is defined` / `is undefined`specially (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 X` → `False`

- `true or X` → `True`

- 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_body`flattens 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]`
