You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
GLR ambiguity-merge is O(n²) for deeply-nested ambiguous grammars (e.g. Perl), even with the recursion-depth cap #471
tree-sitter's GLR ambiguity-merge (stack_node_add_link in internal/cbm/vendored/ts_runtime/src/stack.c) exhibits O(n²)-or-worse parse time on deeply-nested, grammar-ambiguous input — e.g. a Perl f(f(f(...))) chain, where the grammar is ambiguous for paren-optional function calls (ambiguous_function_call_expression).
This is distinct from the stack-overflow crash fixed in #461: the CBM_TS_STACK_MERGE_MAX_DEPTH cap bounds recursion depth (preventing SIGSEGV), but does not bound the total merge work, so parse time still grows superlinearly with nesting depth.
Pre-cap: a 30k-deep ambiguous Perl chain SIGSEGV'd (stack overflow) — even on an 8 MB stack at extreme depth.
Post-cap: no crash, but extracting the same 30k-deep input takes > 5 minutes (would time out CI). The same input at depth 2000 completes in < 1 s — confirming superlinear growth, not the cap.
Unambiguous grammars (Java/C++) parse the identical f(f(...)) depth quickly because they trigger no GLR ambiguity merging.
Impact
Pathological/adversarial deeply-nested ambiguous source can make indexing of a single file extremely slow (effectively a per-file DoS via unbounded work). Real-world code rarely nests this deeply, so normal-repo impact is low.
Bound the total merge work, not just recursion depth — e.g. an overall ambiguity-merge budget / iteration cap with graceful degradation consistent with the existing MAX_LINK_COUNT bail-out, or memoize/short-circuit redundant merges. Goal: keep parse time near-linear on pathological ambiguous input while still producing a valid parse tree.
Refs
Surfaced during the Perl LSP work in #461 (the ts-runtime GLR merge-depth cap commit and the lsp_perl_deep_expression_no_crash test).
Thanks for the careful write up, @halindrome 🙏 And good call separating this from the #461 crash. The depth cap (CBM_TS_STACK_MERGE_MAX_DEPTH) bounds recursion depth but not the total merge work, so pathological ambiguous nesting (your Perl f(f(f(...))) case) still goes superlinear, effectively a per file denial of service on adversarial input even though real code rarely nests this deep.
We agree on direction: bound the total merge work (an ambiguity merge budget or iteration cap with graceful degradation, consistent with the existing MAX_LINK_COUNT bail out) rather than just depth. Since this lives in the vendored parser runtime, we'll keep the change minimal and clearly marked against upstream. Lower priority than the correctness bugs given the low normal repo impact, but a legitimate hardening item. Thanks!
This issue now has a first-class, durable reproduction in the cumulative bug-reproduction suite landing in #667 — a test that pins the exact failing behaviour, so it's tracked and guarded against silent regression. Where #667 also lands the root-cause fix, that's called out in the PR; otherwise the reproduction makes the eventual fix straightforward and verifiable.
The reproduction goes live once #667 merges. (Posted from the release branch's QA pass.)
Activity
DeusData commentedon Jun 18, 2026
Thanks for the careful write up, @halindrome 🙏 And good call separating this from the #461 crash. The depth cap (
CBM_TS_STACK_MERGE_MAX_DEPTH) bounds recursion depth but not the total merge work, so pathological ambiguous nesting (your Perlf(f(f(...)))case) still goes superlinear, effectively a per file denial of service on adversarial input even though real code rarely nests this deep.We agree on direction: bound the total merge work (an ambiguity merge budget or iteration cap with graceful degradation, consistent with the existing
MAX_LINK_COUNTbail out) rather than just depth. Since this lives in the vendored parser runtime, we'll keep the change minimal and clearly marked against upstream. Lower priority than the correctness bugs given the low normal repo impact, but a legitimate hardening item. Thanks!test(repro): GLR blowup #471 + opencode PATHEXT guard #221
test(repro): fix #471 Windows build (unused-function under -Werror)
DeusData commentedon Jun 28, 2026
This issue now has a first-class, durable reproduction in the cumulative bug-reproduction suite landing in #667 — a test that pins the exact failing behaviour, so it's tracked and guarded against silent regression. Where #667 also lands the root-cause fix, that's called out in the PR; otherwise the reproduction makes the eventual fix straightforward and verifiable.
The reproduction goes live once #667 merges. (Posted from the release branch's QA pass.)
fix(repro): resolve markdown/awk/issue471 per maintainer decisions