Skip to content

fix: reconstruct namespace root of dotted {% set ns.attr %} across scopes#1322

Draft
jasmith-hs wants to merge 1 commit into
masterfrom
jasmith_hubspot/phnom-penh
Draft

fix: reconstruct namespace root of dotted {% set ns.attr %} across scopes#1322
jasmith-hs wants to merge 1 commit into
masterfrom
jasmith_hubspot/phnom-penh

Conversation

@jasmith-hs

Copy link
Copy Markdown
Contributor

What

When eager (two-phase) execution defers a dotted namespace assignment like {% set ns.attr = value %}, also reconstruct the namespace root ns in the scope that owns it — matching the behavior that already exists for {% do ns.update(...) %}.

Why

Eager execution renders a template in a first phase and defers anything it can't yet resolve into a reconstructed template that a second phase renders with the full context. For this to stay correct, any variable a deferred region mutates has to be deferred/reconstructed in the scope where it lives, or the first phase reads a stale value and the two phases diverge.

This already worked for in-place collection mutation via {% do %}: {% do list.append(x) %} reads list to call .append, so list is tracked as a used deferred base and is deferred/reconstructed unconditionally up the context chain.

Dotted namespace assignment behaved differently. {% set ns.attr = x %} tracked ns only as a set deferred base, and set bases are deferred only when isInSameScope is true (the same macro-stack). A plain deferred {% if %} or {% for %} shares the macro stack, so that case worked. But when the assignment runs across a macro boundary — or inside a child interpreter context, which is how some host frameworks pre-render fragments — the deferral of ns never reached the scope where ns was defined. The first phase then read the namespace as unmutated, so a later {{ ns.attr }} (or {% if ns.attr %}) resolved against the wrong value and the reconstructed template dropped the mutation entirely.

Fix

When the left-hand side of a {% set %} is a dotted assignment whose root resolves to a Namespace, register that root as a used deferred word in addition to the existing set word. Mutating ns.attr inherently reads ns, so treating the root as a used base is semantically honest, and it reuses the existing, proven propagation/reconstruction path rather than widening the scope gate for all set tags. The instanceof Namespace guard leaves plain-map dotted keys ({% set someMap.field = x %}, which jinjava stores as a literal "someMap.field" key) untouched.

Tests

  • EagerSetTagTest: the namespace root is registered as a used deferred word for both inline and block dotted sets; a non-namespace map root is not registered (guard); a namespace holding a non-serializable attribute value does not throw during reconstruction.
  • EagerTest: end-to-end reconstruction + second-pass parity for a dotted namespace set inside a macro (the cross-scope case that regressed) and inside a {% for %} loop, each asserting the eager result matches a plain non-eager render.

PR description authored by Claude Code.

…scopes

In eager execution, a dotted namespace assignment such as `{% set ns.attr = x %}`
registered the root `ns` only as a *set* deferred base. Set bases are deferred
only within the same scope (the `isInSameScope` macro-stack check), so when the
assignment runs across a macro boundary or inside a child interpreter context,
the deferral of `ns` never reached the scope that owns it. Phase 1 then read the
namespace unmutated and produced output that diverged from a full render.

Register the namespace root as a *used* deferred base as well (only when the root
resolves to a `Namespace`), reusing the same unconditional propagation and
value-reconstruction path that already makes `{% do ns.update(...) %}` correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jasmith-hs jasmith-hs self-assigned this Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant