Skip to content

fix(graph): merge_context index_ref, check_valid_context, pop_states aliasing#212

Merged
chaoming0625 merged 1 commit into
mainfrom
worktree-graph-audit
Jun 11, 2026
Merged

fix(graph): merge_context index_ref, check_valid_context, pop_states aliasing#212
chaoming0625 merged 1 commit into
mainfrom
worktree-graph-audit

Conversation

@chaoming0625

@chaoming0625 chaoming0625 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Audit of brainstate.graph surfaced three correctness bugs, each reproduced with a regression test before fixing.

BUG 1 — merge_context yields a disconnected, empty index_ref

_context.py::merge_context yielded dict(unflatten_ctx.index_ref) — a snapshot taken at yield time (empty) and disconnected from the table treefy_merge actually populates. Asymmetric with split_context, which yields its live RefMap (consumed by graph_to_tree after the block). Fix: yield the live dict.

BUG 2 — Node.check_valid_context raises AttributeError

_node.py::Node.check_valid_context read self._trace_state, but graph nodes (incl. nn.Linear, nn.Module) carry no _trace_state — only State objects do — so it raised AttributeError for every node. (Currently only reached as dead code via check_consistent_aliasing, but it is broken public API.) Fix: a node's validity is the conjunction of the trace validity of the States reachable from it; iterate them and raise TraceContextError on any invalid one.

BUG 3 — pop_states leaves dangling aliases for shared/tied states

_operations.py::_graph_pop deduplicated a matched State by identity and popped only its first reference; later aliases were skipped and survived. After popping a tied weight (baz.weight = bar.weight), bar.weight was removed but baz.weight remained, leaving the node half-mutated. Fix: detach every alias of a popped state, while still recording it once.

Withdrawn after investigation

A suspected KeyError-vs-ValueError contract gap on missing StateLeafEdge paths turned out to be unreachable: TreefyState is itself a JAX pytree, so StateLeafEdge is never produced by flatten (dead code). No change.

Testing

  • New regression tests: 8, all green.
  • brainstate/graph/ — 192 passed
  • brainstate/transform/ — 1136 passed
  • brainstate/nn/ — 1773 passed (14 pre-existing unrelated skips)

🤖 Generated with Claude Code

Summary by Sourcery

Fix multiple graph correctness issues and add regression coverage for context merging, node trace validation, and popping shared states.

Bug Fixes:

  • Ensure merge_context yields the live index_ref table so callers see the populated mapping after tree reconstruction.
  • Fix Node.check_valid_context to validate the trace context of contained State objects instead of accessing a missing node trace field.
  • Update graph state popping to detach all aliases of a popped State, preventing dangling references on nodes.

Tests:

  • Add regression tests for pop_states handling of shared aliases and tied weights while preserving unrelated state pops.
  • Add regression tests ensuring Node.check_valid_context works on modules, plain nodes, stateless nodes, and nested states.
  • Add regression test confirming merge_context exposes a live, populated index_ref mapping.

…aliasing

Audit of brainstate.graph found three correctness bugs:

- merge_context yielded `dict(index_ref)` — an empty snapshot disconnected
  from the table that `treefy_merge` populates. Yield the live dict so it is
  symmetric with `split_context` (whose live `ref_index` is consumed by
  `graph_to_tree`).

- Node.check_valid_context read `self._trace_state`, which graph nodes never
  carry (only `State` objects do), so it raised `AttributeError` for every
  graph node (e.g. `nn.Linear`). A node has no trace state of its own; its
  validity is the conjunction of the trace validity of the `State`s reachable
  from it — iterate them and raise `TraceContextError` on any invalid one.

- pop_states deduplicated a matched `State` by identity and popped only its
  first reference; later shared/tied aliases were skipped and left dangling
  on the node (e.g. `baz.weight = bar.weight` then popping ParamState removed
  `bar.weight` but kept `baz.weight`). Detach every alias of a popped state
  while still recording it once.

Adds regression tests for each. graph (192), transform (1136) and nn (1773)
suites pass.
@chaoming0625 chaoming0625 merged commit 90f3646 into main Jun 11, 2026
5 checks passed
@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Fixes three graph correctness issues: merge_context now yields the live index_ref table instead of a snapshot, Node.check_valid_context validates reachable State objects instead of accessing a nonexistent _trace_state, and _graph_pop/pop_states now fully detaches all aliases of popped states while still deduplicating them by identity; all covered by new regression tests.

Sequence diagram for merge_context yielding live index_ref

sequenceDiagram
    participant Caller
    participant merge_context
    participant GRAPH_CONTEXT

    Caller->>merge_context: enter merge_context()
    merge_context->>GRAPH_CONTEXT: index_ref_stack.append(unflatten_ctx)
    merge_context-->>Caller: yield unflatten_ctx, index_ref
    Caller->>index_ref: update index_to_object_mappings
    Caller-->>merge_context: exit context
    merge_context->>GRAPH_CONTEXT: index_ref_stack.pop()
    merge_context->>unflatten_ctx: del index_ref
Loading

Sequence diagram for _graph_pop detaching shared State aliases

sequenceDiagram
    participant Caller
    participant _graph_pop
    participant impl
    participant graph_node

    Caller->>_graph_pop: _graph_pop(graph_node, id_to_index,...)
    _graph_pop->>graph_node: traverse children
    _graph_pop->>graph_node: visit first State_leaf
    _graph_pop->>id_to_index: add id(State)
    _graph_pop->>impl: pop_key(graph_node, name_first)

    _graph_pop->>graph_node: visit second alias of same State
    _graph_pop->>id_to_index: check id(State) in id_to_index
    alt [id(State) already in id_to_index]
        _graph_pop->>impl: pop_key(graph_node, name_alias)
        _graph_pop-->>Caller: do not record State again
    end
Loading

File-Level Changes

Change Details Files
Node.check_valid_context now validates the trace context of all reachable State objects rather than relying on a nonexistent node-level trace state.
  • Import iter_leaf from ._walk to traverse node leaves
  • Rewrite check_valid_context to iterate over iter_leaf(self) and look for State instances
  • Raise TraceContextError using error_msg() when any contained State has an invalid _trace_state
  • Expand docstring to clarify semantics and laziness of error_msg
brainstate/graph/_node.py
merge_context yields the live index_ref mapping so callers can inspect the rebuilt-object table after treefy_merge, mirroring split_context behavior.
  • Change merge_context to yield the original index_ref dict instead of dict(unflatten_ctx.index_ref)
  • Document in comments that the live table is exposed for post-block inspection
  • Add a regression test ensuring index_ref is populated and contains the rebuilt root object
brainstate/graph/_context.py
brainstate/graph/_context_test.py
Graph pop/pop_states now remove all aliases of a matched State while still recording the State only once.
  • Update _graph_pop to early-continue on non-node leaves and to detect when a State id is already in id_to_index
  • When encountering an already-popped shared/tied State, call impl.pop_key to detach the alias from the node without re-recording it
  • Keep the original behavior of deduplicating States by identity for recording
  • Add regression tests for plain nodes with shared attributes, tied weights in nn.Modules, and non-shared states to ensure behavior is unchanged otherwise
brainstate/graph/_operations.py
brainstate/graph/_operation_test.py
Add regression coverage for Node.check_valid_context to ensure it no longer raises AttributeError and correctly validates nested and stateless nodes.
  • Introduce TestCheckValidContext test case class with multiple scenarios
  • Test check_valid_context on a real brainstate.nn.Linear module in a non-traced context
  • Test nodes with ParamState, stateless nodes, and nodes containing nested modules/states to ensure they all pass when context is valid
brainstate/graph/_node_test.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chaoming0625 chaoming0625 deleted the worktree-graph-audit branch June 11, 2026 14:18

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="brainstate/graph/_context_test.py" line_range="105-114" />
<code_context>
             self.assertIsNotNone(mctx)
         self.assertFalse(hasattr(mctx, 'index_ref'))

+    def test_merge_context_exposes_live_index_ref(self):
+        """``merge_context`` must yield the *live* index_ref table.
+
+        Regression: it previously yielded ``dict(unflatten_ctx.index_ref)`` — a
+        snapshot taken at yield time (empty) and disconnected from the table that
+        ``treefy_merge`` actually populates. This is now symmetric with
+        ``split_context``, which yields its live ``RefMap``.
+        """
+        m = brainstate.nn.Linear(2, 3)
+        graphdef, state = brainstate.graph.treefy_split(m)
+        with brainstate.graph.merge_context() as (ctx, index_ref):
+            rebuilt = ctx.treefy_merge(graphdef, state)
+        # The yielded table is populated and contains the rebuilt root object.
+        self.assertGreater(len(index_ref), 0)
+        self.assertIn(id(rebuilt), {id(v) for v in index_ref.values()})
+

</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen `merge_context` regression test by asserting that the yielded `index_ref` is the live table, not just populated.

The current assertions only show that `index_ref` is populated with the rebuilt root, but not that it’s the same live mapping held by `MergeContext` rather than a filled copy. To better protect against regressions to the old snapshot behavior, add an identity-style assertion (e.g. `self.assertIs(ctx.index_ref, index_ref)` if applicable) or another invariant that specifically distinguishes a live reference from a snapshot.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +105 to +114
def test_merge_context_exposes_live_index_ref(self):
"""``merge_context`` must yield the *live* index_ref table.

Regression: it previously yielded ``dict(unflatten_ctx.index_ref)`` — a
snapshot taken at yield time (empty) and disconnected from the table that
``treefy_merge`` actually populates. This is now symmetric with
``split_context``, which yields its live ``RefMap``.
"""
m = brainstate.nn.Linear(2, 3)
graphdef, state = brainstate.graph.treefy_split(m)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen merge_context regression test by asserting that the yielded index_ref is the live table, not just populated.

The current assertions only show that index_ref is populated with the rebuilt root, but not that it’s the same live mapping held by MergeContext rather than a filled copy. To better protect against regressions to the old snapshot behavior, add an identity-style assertion (e.g. self.assertIs(ctx.index_ref, index_ref) if applicable) or another invariant that specifically distinguishes a live reference from a snapshot.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.00000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
brainstate/graph/_node.py 50.00% 1 Missing and 1 partial ⚠️
brainstate/graph/_operations.py 80.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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