Fix Transformer_NonRecursive corrupting the tree on a non-last Discard#1622
Open
chuenchen309 wants to merge 1 commit into
Open
Conversation
When a callback returned Discard, Transformer_NonRecursive did not push a result for that node, but a parent still sliced `stack[-len(children):]`. For a discarded node that is not the last child processed, that slice over-consumed the stack and pulled an unrelated sibling into the wrong parent -- e.g. `start[keep, wrap[drop]]` came back as `start[wrap[keep]]`, silently dropping and re-parenting nodes. The base Transformer (documented to give the same result) was unaffected. Push discarded results as placeholders so every node consumes exactly `len(children)` stack slots, and filter the placeholders when building a parent's args. A wholly-discarded tree now returns None (this also covers the root-discard crash) instead of mis-unpacking an empty stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: chuenchen309 <48723787+chuenchen309@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Transformer_NonRecursivecorrupts the tree when a callback returnsDiscardfor a node that is not the last child processed. It slices its internal stack bylen(x.children), but discarded children were never pushed onto that stack, so the slice over-consumes and pulls an unrelated sibling into the wrong parent:keepis silently dropped fromstartand re-parented underwrap.Transformer_NonRecursiveis documented as "Same as Transformer", and the baseTransformer(unaffected) is the correct twin. A differential run of 3000 random trees usingDiscardgave 307 mismatches vs the baseTransformerbefore this change, 0 after.Cause
In
Transformer_NonRecursive.transform,size = len(x.children)counts all original children, but a child whose callback returnedDiscardwas never pushed (if res is not Discard: stack.append(res)), sostack[-size:]reaches into earlier siblings.Fix
Push discarded results as placeholders so every node consumes exactly
len(children)slots, and filter the placeholders when building a parent's args. A wholly-discarded tree now returnsNoneinstead of mis-unpacking an empty stack.Existing tests pass (
test_transformer_variantsonly exercisedDiscardon a last child, which masked this); addedtest_transformer_variants_discard_with_preceding_siblingcovering all four transformer variants.mypyclean.Note on #1614
As a side effect of the stack-alignment fix, a fully-discarded root now returns
None— the same case #1614 targets for the crash. The interior-sibling corruption fixed here is distinct (it survives #1614'sif not stackguard), but there is overlap on the root case; happy to rebase around #1614 or adjust if you'd prefer to keep them separate.Disclosure: authored by an AI coding agent (Claude Code) running on this account — it found the bug, wrote the repro/fuzz, the fix and this description. I review every change and am accountable for it; the verification above is real and re-runnable from the diff. If this isn't a change you want, say so and I'll close it.