Skip to content

Fix Transformer_NonRecursive corrupting the tree on a non-last Discard#1622

Open
chuenchen309 wants to merge 1 commit into
lark-parser:masterfrom
chuenchen309:fix/nonrecursive-transformer-discard-sibling
Open

Fix Transformer_NonRecursive corrupting the tree on a non-last Discard#1622
chuenchen309 wants to merge 1 commit into
lark-parser:masterfrom
chuenchen309:fix/nonrecursive-transformer-discard-sibling

Conversation

@chuenchen309

Copy link
Copy Markdown

Problem

Transformer_NonRecursive corrupts the tree when a callback returns Discard for a node that is not the last child processed. It slices its internal stack by len(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:

from lark import Tree, Token
from lark.visitors import Transformer, Transformer_NonRecursive, Discard

class Drop:
    def drop(self, children): return Discard

tree = Tree('start', [Tree('keep', [Token('T', 'x')]), Tree('wrap', [Tree('drop', [])])])

class TBase(Drop, Transformer): pass
class TNR(Drop, Transformer_NonRecursive): pass

TBase().transform(tree)   # Tree('start', [Tree('keep', [Token('T','x')]), Tree('wrap', [])])   (correct)
TNR().transform(tree)     # Tree('start', [Tree('wrap', [Tree('keep', [Token('T','x')])])])       (WRONG)

keep is silently dropped from start and re-parented under wrap. Transformer_NonRecursive is documented as "Same as Transformer", and the base Transformer (unaffected) is the correct twin. A differential run of 3000 random trees using Discard gave 307 mismatches vs the base Transformer before this change, 0 after.

Cause

In Transformer_NonRecursive.transform, size = len(x.children) counts all original children, but a child whose callback returned Discard was never pushed (if res is not Discard: stack.append(res)), so stack[-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 returns None instead of mis-unpacking an empty stack.

Existing tests pass (test_transformer_variants only exercised Discard on a last child, which masked this); added test_transformer_variants_discard_with_preceding_sibling covering all four transformer variants. mypy clean.

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's if not stack guard), 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.

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