Skip to content

Fix deeply nested quantifiers/alternations exceeding CPython CO_MAXBLOCKS - #396

Open
piotr-oles wants to merge 1 commit into
renatahodovan:masterfrom
piotr-oles:fix/deep-nesting-py
Open

piotr-oles wants to merge 1 commit into
renatahodovan:masterfrom
piotr-oles:fix/deep-nesting-py

Conversation

@piotr-oles

@piotr-oles piotr-oles commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Grammarinator renders every grammar rule as a single Python method, inlining sub-structure recursively. Each quantifier opens 3 nested blocks (with + while + with) and each alternation 1 (with). CPython caps statically-nested blocks per code object at CO_MAXBLOCKS (20), so a deeply nested rule overflows the limit and the generated module raises SyntaxError: too many statically nested blocks at import time.

This PR splits any over-budget rule (Python target only) into synthetic fragment helper methods: the deepest over-budget quantifier/alternation is cut out into its own def (block depth 0) and a call is left behind. Extraction is safe because current == rule.current at every sibling position.

The work is in two commits:

  1. Extract grammar model from processor into model module — pure refactor, no behaviour change. The node/graph representation moves out of processor.py into grammarinator/tool/model.py; processor.py and parser.py import from it.
  2. Fix deeply nested quantifiers/alternations — the actual fix: new grammarinator/tool/splitter.py, the FragmentRuleNode/FragmentRefNode model nodes, template support, and tests.

Before / After

Example grammar (tests/grammars/DeepNesting.g4), deepquant rule — 11 nested optional quantifiers with a label (v=) deep inside, forcing a local_ctx:

deepquant
  : ('a' ('b' ('c' ('d' ('e' ('f' ('g' ('h' ('i' (v='j' 'end'?)?)?)?)?)?)?)?)?)?)?
  ;

Before — one method, nested to block depth 34 → fails at import

def deepquant(self, parent: ParentRule | None = None) -> Rule:
    local_ctx = {'v': None}
    with UnparserRuleContext(self, 'deepquant', parent) as rule:
        current = rule.current
        with QuantifierContext(rule, 0, 0, 1, ..., 0) as quant0:
            while quant0():
                with QuantifiedContext(rule):
                    current = rule.current
                    self.T__0(parent=current)
                    with QuantifierContext(rule, 1, 0, 1, ..., 0) as quant1:
                        while quant1():
                            with QuantifiedContext(rule):
                                current = rule.current
                                self.T__1(parent=current)
                                # ... quantifiers 2..10 keep nesting, 3 blocks each ...
                                #     until the innermost `local_ctx['v'] = ...`
    return current
$ python -c "import DeepNestingGenerator"
    with QuantifiedContext(rule):
         ^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: too many statically nested blocks

(deepalt, with 23 nested alternations, reaches block depth 24 and fails the same way.)

After — deepest over-budget node cut into a fragment; module imports cleanly

def deepquant(self, parent: ParentRule | None = None) -> Rule:
    local_ctx = {'v': None}
    with UnparserRuleContext(self, 'deepquant', parent) as rule:
        current = rule.current
        with QuantifierContext(rule, 0, 0, 1, ..., 0) as quant0:
            while quant0():
                with QuantifiedContext(rule):
                    current = rule.current
                    self.T__0(parent=current)
                    # ... quantifiers 1..5 ...
                    with QuantifierContext(rule, 5, 0, 1, ..., 0) as quant5:
                        while quant5():
                            with QuantifiedContext(rule):
                                current = rule.current
                                self.T__5(parent=current)
                                self._deepquant_frag_0(rule, local_ctx)   # <-- cut point
                                current = rule.current
    return current

def _deepquant_frag_0(self, rule, local_ctx) -> None:
    current = rule.current
    with QuantifierContext(rule, 6, 0, 1, ..., 0) as quant6:
        while quant6():
            with QuantifiedContext(rule):
                current = rule.current
                self.T__6(parent=current)
                # ... quantifiers 7..9, then the labelled element ...
                                        local_ctx['v'] = current.last_child   # local_ctx threaded in
                # ... quantifier 10 ...
    current = rule.current
$ python -c "import DeepNestingGenerator"
$   # imports cleanly

The local_ctx dict is threaded into the fragment as a parameter, so labels/args/locals/returns still resolve after the cut.

Notes

  • I recommend reviewing commit by commit. First commit splits the processor.py, second adds the fix for nesting
  • Python target only — C++ has no equivalent limit.
  • splitter._block_cost mirrors GeneratorTemplate.py.jinja and must stay in sync with it; the test uses an independent AST-based oracle to guard against drift.
  • Known limitation: user action code (@init, @after, etc.) block statements are not counted, so extremely deep action-heavy rules can still overflow.

@piotr-oles
piotr-oles force-pushed the fix/deep-nesting-py branch 2 times, most recently from db6c650 to b6fe6f4 Compare July 3, 2026 13:41
@piotr-oles
piotr-oles marked this pull request as ready for review July 3, 2026 13:46
@piotr-oles

Copy link
Copy Markdown
Contributor Author

Hey @renatahodovan , thanks for building this tool, I found it very useful for fuzz testing ANTLR parsers. Looking forward for your review 🙇🏻

@renatahodovan renatahodovan left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the analysis is spot on. The approach (cutting deep sub-expressions into generated helper methods) is the right one. Before merging I'd like to shrink the implementation, see comments below.

Comment thread grammarinator/tool/grammar.py
Comment thread grammarinator/tool/splitter.py Outdated
Comment thread grammarinator/tool/model.py Outdated
Comment thread grammarinator/tool/resources/codegen/GeneratorTemplate.py.jinja Outdated
Comment thread grammarinator/tool/resources/codegen/GeneratorTemplate.py.jinja Outdated
Comment thread grammarinator/tool/resources/codegen/GeneratorTemplate.py.jinja Outdated
Comment thread grammarinator/tool/model.py Outdated
Comment thread tests/test_processor_nesting.py Outdated
Comment thread tests/grammars/DeepNesting.g4
Comment thread RELNOTES.rst Outdated
@piotr-oles
piotr-oles force-pushed the fix/deep-nesting-py branch from b6fe6f4 to 7d8de64 Compare August 11, 2026 09:27
@coveralls

coveralls commented Aug 11, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 83.554% (+0.4%) from 83.127% — piotr-oles:fix/deep-nesting-py into renatahodovan:master

@piotr-oles

Copy link
Copy Markdown
Contributor Author

Hey @renatahodovan ! I addressed your feedback, could you give it another look?

@akosthekiss akosthekiss left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me chime in. Thanks for the update. I have some suggestions for simplifying the additions and to get them more aligned with existing code.

Comment thread grammarinator/tool/grammar.py Outdated
Comment thread grammarinator/tool/grammar.py Outdated
Comment thread grammarinator/tool/grammar.py Outdated
Comment thread grammarinator/tool/processor.py Outdated
Comment thread grammarinator/tool/processor.py Outdated
@akosthekiss

Copy link
Copy Markdown
Collaborator

Hi @piotr-oles , I have pushed a commit into the PR, I hope you don't mind. This shows the idea behind by review better, hopefully. I have executed some tests locally, and this version generated the same code for DeepNesting.g4 as the original PR, at least. However, you surely have real use cases, so I'd like to ask you to check these changes on your side as well. Please, do edit the code further as necessary. (One more thing: once the PR gets close to completion, please squash all commits into one and rebase it on top of the master branch.)

@piotr-oles

Copy link
Copy Markdown
Contributor Author

Hi @akosthekiss , thanks for review and changes! Sorry for the delay, I was on vacations :)

I will test it against the real grammar we have, if everything works, I will rebase and squash, so we're ready for the merge.

…OCKS

CPython caps statically-nested blocks per code object at CO_MAXBLOCKS
(20). Grammarinator renders each rule as one method, inlining
quantifiers (3 blocks each) and alternations (1 block each) recursively,
so deeply nested rules overflow the limit and make the generated module
raise 'SyntaxError: too many statically nested blocks' at import.
@piotr-oles

Copy link
Copy Markdown
Contributor Author

I confirm this works after all the changes 👍 I squashed all commits and they're up to date with master. @renatahodovan , @akosthekiss could you take another look? 🙏🏻

@piotr-oles

Copy link
Copy Markdown
Contributor Author

Hey! Is there anything blocking from merging this? :)

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.

4 participants