Skip to content

Fix walk skipping children of nodes displaced by an insertion - #1790

Draft
TwitchBronBron wants to merge 1 commit into
masterfrom
fix-walk-displaced-nodes
Draft

Fix walk skipping children of nodes displaced by an insertion#1790
TwitchBronBron wants to merge 1 commit into
masterfrom
fix-walk-displaced-nodes

Conversation

@TwitchBronBron

Copy link
Copy Markdown
Member

When a visitor inserts a statement above the node currently being walked, that node's children are silently never walked. Injecting a print above an if means the if body never gets visited.

walk re-reads owner[key] after the visitor runs and treats any change as "this node was replaced". But three different mutations all produce that same observation, and only one of them is a replacement:

Mutation owner[key] changed? Original node
Replace in place yes gone from array
Insert above yes still in array, moved down
Delete yes gone, array shrank

Insert-above was being misread as replacement, so walk descended into the newly-inserted node and walkArray marked the original processed — skipping it forever.

What changed

  • Split the three cases apart in a new internal walkSlot, using array membership (indexOf) plus length change to tell them apart. Public walk() keeps its exact signature.
  • walkArray marks a node processed before walking it, so one visit per node regardless of how the visitor mutates the array afterwards.
  • walkArray now checks the cancellation token in its loop; previously a cancelled walk kept spinning the array, only bailing inside each walk call.

Verified

npm test — 3088 passing, 0 failing. Reverting just visitors.ts while keeping the new tests fails 4 of them, so they do pin the bug rather than describe the implementation.

Not fixed — needs a design decision

Two mutation shapes are still wrong, added as it.skip with the actual-vs-expected recorded:

  • splice(key, 1, x, y) (delete one, insert several) — x is never visited. Length is unchanged-or-larger, so "replaced by x" and "deleted, then x and y inserted" are indistinguishable after the fact.
  • Reordering — a node moved backwards past the cursor is never revisited; one moved forwards is skipped. There's no defined semantic for reordering mid-walk today.

These aren't fixable by better inference, because the right answer depends on what the plugin intends and that isn't in the AST. Two legitimate, mutually exclusive expectations exist today:

  • fixpoint — keep walking until nothing changes (desugaring passes want this)
  • single-pass — visit each original node once; injected nodes are output, not input (instrumentation wants this, and it's also what makes injection terminate — a fixpoint walk whose visitor always injects never ends)

Suggested follow-up: a WalkOptions.injectedNodes?: 'skip' | 'visit' flag plus a maxPasses safety valve, implemented by snapshotting array membership before the loop instead of inferring from index math. That approach also dissolves both gaps above and removes the i = -1 O(n²) restart. Default should stay 'visit' for now to avoid a breaking change, with a flip to 'skip' considered for the next major.

Worth noting AstEditor already records every mutation with exact indices — if the walker consumed that change log it would know rather than infer, but only when an editor is passed, and much of the codebase splices arrays raw.

Drafting rather than opening for review since the follow-up design is the more important half of this.

When a visitor inserted a statement above the node being walked, `walk` re-read
`owner[key]`, saw a different node, and assumed the original had been replaced.
It then descended into the newly-inserted node instead, and `walkArray` marked
the original as processed — so the displaced node's children were never walked.

`walk` observes only the before/after state of a single slot, which cannot
distinguish replacement from displacement from deletion. Split those three cases
apart explicitly via array membership and length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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