Skip to content

gh-158847: Avoid enumerating cycles when choosing pegen recursion leaders - #158848

Open
jonbaldie wants to merge 1 commit into
python:mainfrom
jonbaldie:perf-pegen-cycle-leaders
Open

jonbaldie wants to merge 1 commit into
python:mainfrom
jonbaldie:perf-pegen-cycle-leaders

Conversation

@jonbaldie

@jonbaldie jonbaldie commented Oct 5, 2026 •

Copy link
Copy Markdown

Summary

To pick a leader for a left-recursive SCC, pegen walked every path from every node in the SCC. The number of paths grows factorially with the SCC size. A leader is a rule that sits on every cycle, so we can test each candidate directly: remove it and check whether what's left is acyclic.

 compute_left_recursives
   for each SCC with >1 rule
-    for start in scc
-      for cycle in find_cycles_in_scc(start)   # every path, O(n!)
-        leaders -= scc - set(cycle)
-    leader = min(leaders)
+    for leader in sorted(scc)
+      if is_acyclic(scc - {leader})          # Kahn's toposort, O(V+E)
+        mark leader; break
+    else
+      raise ValueError("no leadership candidate ...")

Overall that's O(V·(V+E)) per SCC. It picks the same leader as before (smallest name that lies on every cycle) and raises the same error. find_cycles_in_scc had no other callers, so I removed it.

CPython's own grammar barely notices this, because its biggest SCC has two rules. Generated Parser/parser.c is byte-identical before and after. Grammars with wider mutual left recursion hit a wall, though.

Evidence

Parser generation, median of 7, release build, macOS arm64. n is the number of mutually left-recursive rules:

case before after
valid grammar, n=10 30.2 ms 4.4 ms
valid grammar, n=14 2,515 ms 7.9 ms
no-leader grammar, n=9 481 ms 3.9 ms
Grammar/python.gram 51.8 ms 52.3 ms (noise)

New test test_left_recursion_analysis_work counts name comparisons on a fully connected 8-rule grammar that has no leader:

  • Before: AssertionError: 1067658 not less than 2000
  • After: 1,272 comparisons, passes

Also added test_left_recursion_leader_order (leader choice stays the same) and test_large_left_recursive_grammar (12-rule SCC still generates a working parser).

I checked the old and new compute_left_recursives against each other on all 512 three-rule graphs and 256 random four-rule graphs. Flags, SCCs and errors matched every time. test_peg_generator -u cpu: 125 run, all pass.

Merge Danger

Door: two-way

Build-time tooling only. Generated parser output is unchanged.

Blast Radius: tooling

This only matters to people running pegen on their own grammars. For them, leader choice and error messages stay the same.

AI disclosure: the profiling, the patch and the equivalence check were done with AI tools (Codex, Claude).

🤖 Generated with Claude Code

…on leaders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@python-cla-bot

python-cla-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant