Skip to content

Commit dae9801

Browse files
committed
avoid quadratic tree traversal in pre_order/post_order/leaves
1 parent 51abf53 commit dae9801

2 files changed

Lines changed: 31 additions & 8 deletions

File tree

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@
127127
search for each converted block within its parent's child list from the previous
128128
conversion's position instead of rescanning the whole list from the start on every
129129
node removal (#5232)
130+
- Improve performance on deeply nested expressions (such as a long `a ** b ** c ** ...`
131+
chain) by walking the `blib2to3` node tree iteratively in `pre_order`, `post_order` and
132+
`leaves` instead of recursing with `yield from`, whose per-node generator delegation
133+
made a full traversal quadratic in nesting depth (#XXXX)
130134

131135
### Output
132136

src/blib2to3/pytree.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,16 @@ def prev_sibling(self) -> NL | None:
217217
return self.parent.prev_sibling_map[id(self)]
218218

219219
def leaves(self) -> Iterator["Leaf"]:
220-
for child in self.children:
221-
yield from child.leaves()
220+
# Walk iteratively rather than recursing with `yield from`, whose per-node
221+
# generator delegation is O(depth) and makes a full traversal quadratic on
222+
# deeply nested trees (e.g. a long `a ** b ** c ** ...` chain).
223+
stack: list[NL] = list(reversed(self.children))
224+
while stack:
225+
node = stack.pop()
226+
if isinstance(node, Leaf):
227+
yield node
228+
else:
229+
stack.extend(reversed(node.children))
222230

223231
def depth(self) -> int:
224232
if self.parent is None:
@@ -301,15 +309,26 @@ def clone(self) -> "Node":
301309

302310
def post_order(self) -> Iterator[NL]:
303311
"""Return a post-order iterator for the tree."""
304-
for child in self.children:
305-
yield from child.post_order()
306-
yield self
312+
# Walk iteratively rather than recursing with `yield from`. The recursive
313+
# form resumes one delegating generator per level on every step, so a full
314+
# traversal costs O(depth) per node and is quadratic on deeply nested trees.
315+
stack: list[tuple[NL, bool]] = [(self, False)]
316+
while stack:
317+
node, expanded = stack.pop()
318+
if expanded:
319+
yield node
320+
else:
321+
stack.append((node, True))
322+
stack.extend((child, False) for child in reversed(node.children))
307323

308324
def pre_order(self) -> Iterator[NL]:
309325
"""Return a pre-order iterator for the tree."""
310-
yield self
311-
for child in self.children:
312-
yield from child.pre_order()
326+
# See `post_order` for why this is iterative instead of recursive.
327+
stack: list[NL] = [self]
328+
while stack:
329+
node = stack.pop()
330+
yield node
331+
stack.extend(reversed(node.children))
313332

314333
@property
315334
def prefix(self) -> str:

0 commit comments

Comments
 (0)