Skip to content

Fix parser stack overflow in ordinary repetition - #105

Merged
tinovyatkin merged 3 commits into
mainfrom
codex/fix-star-loop-stack-overflow
Jul 18, 2026
Merged

Fix parser stack overflow in ordinary repetition#105
tinovyatkin merged 3 commits into
mainfrom
codex/fix-star-loop-stack-overflow

Conversation

@tinovyatkin

Copy link
Copy Markdown
Contributor

Summary

  • walk ordinary StarLoopEntry and PlusLoopBack repetition with an explicit heap work list so loop length no longer consumes native stack
  • preserve speculative parse-tree order with compact parser-arena rope IDs and iterative materialization instead of per-iteration Rc allocations
  • cover * and +, child-rule bodies, tree order and spans, reduced-stack execution, long inputs, exact linear arena growth, and arena reset/reuse

Root cause and impact

recognize_state_fast recursively followed ordinary loop-back edges. Recursion depth therefore grew linearly with the number of iterations, so a valid MySQL INSERT ... VALUES statement overflowed the process stack after roughly 200 tuples and the 12,376-tuple benchmark fixture aborted outright.

The runtime now keeps repetition continuations on the heap while preserving ANTLR transition ordering and backtracking behavior. Long valid repetitions parse on the default stack, and deferred tree storage remains linear in input length.

Performance

Same-machine, interleaved measurements against the committed stack-safe baseline:

  • MySQL 12,376-tuple reproducer median: 404.6 ms -> 393.2 ms (2.8% faster)
  • Kotlin parity smoke fixtures: median average parse time improved by approximately 6-9%

The pre-fix recursive implementation cannot complete this input correctly even on a 512 MiB thread stack: it reaches the recognizer depth guard after about 45 seconds.

Validation

  • cargo fmt --check -- src/parser.rs
  • cargo test --locked --lib (196 passed)
  • cargo test --locked --bin antlr4-rust-gen (163 passed)
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • cargo run --release --quiet --bin antlr4-runtime-testsuite
    • 357 passed, 0 failed, 0 skipped, 357 run
  • MySQL 12,376-tuple reproducer parses successfully on the default stack

Closes #103.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Copy/Paste Detection

Found 15 duplication(s) across 1 changed Rust file(s) (threshold: 100 tokens).

Show duplications

Found a 27 line (145 tokens) duplication in the following files:

  • Starting at line 13554 of src/parser.rs
  • Starting at line 13687 of src/parser.rs
    fn generated_match_token_recovers_missing_token_from_context_follow() {
        let atn = generated_match_recovery_atn();
        let data = RecognizerData::new(
            "Mini.g4",
            Vocabulary::new(
                [None, Some("'X'"), Some("'Y'")],
                [None, Some("X"), Some("Y")],
                [None::<&str>, None, None],
            ),
        );
        let mut parser = BaseParser::new(
            CommonTokenStream::new(Source {
                tokens: vec![TestToken::eof("parser-test", 3, 1, 3)],
                index: 0,
            }),
            data,
        );
        parser.rule_context_stack = vec![
            RuleContextFrame {
                rule_index: 0,
                invoking_state: 0,
            },
            RuleContextFrame {
                rule_index: 1,
                invoking_state: 1,
            },
        ];

Found a 27 line (127 tokens) duplication in the following files:

  • Starting at line 12574 of src/parser.rs
  • Starting at line 12646 of src/parser.rs
        let mut atn = ParserAtnBuilder::new(2);
        assert_eq!(
            atn.add_state(AtnStateKind::RuleStart, Some(0))
                .expect("state")
                .index(),
            0
        );
        assert_eq!(
            atn.add_state(AtnStateKind::BlockStart, Some(0))
                .expect("state")
                .index(),
            1
        );
        assert_eq!(
            atn.add_state(AtnStateKind::Basic, Some(0))
                .expect("state")
                .index(),
            2
        );
        assert_eq!(
            atn.add_state(AtnStateKind::Basic, Some(0))
                .expect("state")
                .index(),
            3
        );
        assert_eq!(
            atn.add_state(AtnStateKind::BlockEnd, Some(0))

Found a 33 line (126 tokens) duplication in the following files:

  • Starting at line 7879 of src/parser.rs
  • Starting at line 7917 of src/parser.rs
                    if self.fast_parser_predicate_matches(predicate_context, transition, index) {
                        let boundary = left_recursive_boundary(atn, state, target);
                        outcomes.extend(
                            self.recognize_state_fast(
                                atn,
                                FastRecognizeRequest {
                                    state_number: target,
                                    stop_state,
                                    index,
                                    rule_start_index,
                                    decision_start_index: next_decision_start_index,
                                    precedence,
                                    depth: depth + 1,
                                    recovery_symbols: Rc::clone(&epsilon_recovery_symbols),
                                    recovery_state: epsilon_recovery_state,
                                },
                                FastRecognizeScratch {
                                    predicate_context,
                                    visiting,
                                    memo,
                                    expected,
                                },
                            )
                            .into_iter()
                            .map(|mut outcome| {
                                if let Some(rule_index) = boundary {
                                    let boundary = self.arena_boundary_node(rule_index);
                                    self.defer_fast_outcome_node(&mut outcome, boundary);
                                }
                                outcome
                            }),
                        );
                    } else {

Found a 32 line (125 tokens) duplication in the following files:

  • Starting at line 7844 of src/parser.rs
  • Starting at line 7880 of src/parser.rs
  • Starting at line 7918 of src/parser.rs
                    let boundary = left_recursive_boundary(atn, state, target);
                    outcomes.extend(
                        self.recognize_state_fast(
                            atn,
                            FastRecognizeRequest {
                                state_number: target,
                                stop_state,
                                index,
                                rule_start_index,
                                decision_start_index: next_decision_start_index,
                                precedence,
                                depth: depth + 1,
                                recovery_symbols: Rc::clone(&epsilon_recovery_symbols),
                                recovery_state: epsilon_recovery_state,
                            },
                            FastRecognizeScratch {
                                predicate_context,
                                visiting,
                                memo,
                                expected,
                            },
                        )
                        .into_iter()
                        .map(|mut outcome| {
                            if let Some(rule_index) = boundary {
                                let boundary = self.arena_boundary_node(rule_index);
                                self.defer_fast_outcome_node(&mut outcome, boundary);
                            }
                            outcome
                        }),
                    );
                }

Found a 22 line (125 tokens) duplication in the following files:

  • Starting at line 12600 of src/parser.rs
  • Starting at line 12672 of src/parser.rs
            atn.add_state(AtnStateKind::BlockEnd, Some(0))
                .expect("state")
                .index(),
            4
        );
        assert_eq!(
            atn.add_state(AtnStateKind::RuleStop, Some(0))
                .expect("state")
                .index(),
            5
        );
        atn.set_rule_to_start_state(vec![0])
            .expect("rule start states");
        atn.set_rule_to_stop_state(vec![5])
            .expect("rule stop states");
        atn.add_decision_state(1).expect("decision state");
        atn.add_transition(0, ParserTransitionSpec::Epsilon { target: 1 })
            .expect("transition");
        atn.add_transition(
            1,
            ParserTransitionSpec::Atom {
                target: 2,

Found a 33 line (115 tokens) duplication in the following files:

  • Starting at line 8798 of src/parser.rs
  • Starting at line 8871 of src/parser.rs
                        outcomes.extend(
                            self.recognize_state(
                                atn,
                                RecognizeRequest {
                                    state_number: *target,
                                    stop_state,
                                    index,
                                    rule_start_index,
                                    decision_start_index: next_decision_start_index,
                                    init_action_rules,
                                    predicates,
                                    semantics,
                                    rule_args,
                                    member_actions,
                                    return_actions,
                                    local_int_arg,
                                    member_values: member_values.clone(),
                                    return_values: return_values.clone(),
                                    rule_alt_number: next_alt_number,
                                    track_alt_numbers,
                                    consumed_eof,
                                    precedence,
                                    depth: depth + 1,
                                    recovery_symbols: epsilon_recovery_symbols.clone(),
                                    recovery_state: epsilon_recovery_state,
                                },
                                visiting,
                                memo,
                                expected,
                            )
                            .into_iter()
                            .map(|mut outcome| {
                                prepend_decision(&mut outcome, decision);

Found a 15 line (113 tokens) duplication in the following files:

  • Starting at line 13612 of src/parser.rs
  • Starting at line 13896 of src/parser.rs
    fn generated_match_token_counts_single_token_deletion_recovery() {
        let atn = generated_match_recovery_atn();
        let data = RecognizerData::new(
            "Mini.g4",
            Vocabulary::new(
                [None, Some("'X'"), Some("'Y'"), Some("'Z'")],
                [None, Some("X"), Some("Y"), Some("Z")],
                [None::<&str>, None, None, None],
            ),
        );
        let mut parser = BaseParser::new(
            CommonTokenStream::new(Source {
                tokens: vec![
                    TestToken::new(3).with_text("z"),
                    TestToken::new(2).with_text("y"),

Found a 22 line (112 tokens) duplication in the following files:

  • Starting at line 13011 of src/parser.rs
  • Starting at line 13212 of src/parser.rs
    fn predicate_after_token_atn() -> Atn {
        let mut atn = ParserAtnBuilder::new(2);
        assert_eq!(
            atn.add_state(AtnStateKind::RuleStart, Some(0))
                .expect("state")
                .index(),
            0
        );
        assert_eq!(
            atn.add_state(AtnStateKind::Basic, Some(0))
                .expect("state")
                .index(),
            1
        );
        assert_eq!(
            atn.add_state(AtnStateKind::Basic, Some(0))
                .expect("state")
                .index(),
            2
        );
        assert_eq!(
            atn.add_state(AtnStateKind::Basic, Some(0))

Found a 22 line (111 tokens) duplication in the following files:

  • Starting at line 11799 of src/parser.rs
  • Starting at line 13011 of src/parser.rs
  • Starting at line 13212 of src/parser.rs
    fn left_recursive_loop_with_caller_follow_atn(caller_symbol: i32) -> Atn {
        let mut atn = ParserAtnBuilder::new(2);
        assert_eq!(
            atn.add_state(AtnStateKind::RuleStart, Some(0))
                .expect("state")
                .index(),
            0
        );
        assert_eq!(
            atn.add_state(AtnStateKind::Basic, Some(0))
                .expect("state")
                .index(),
            1
        );
        assert_eq!(
            atn.add_state(AtnStateKind::Basic, Some(0))
                .expect("state")
                .index(),
            2
        );
        assert_eq!(
            atn.add_state(AtnStateKind::RuleStart, Some(1))

Found a 14 line (110 tokens) duplication in the following files:

  • Starting at line 13345 of src/parser.rs
  • Starting at line 15462 of src/parser.rs
    fn parser_matches_token_and_reports_mismatch() {
        let source = Source {
            tokens: vec![
                TestToken::new(1).with_text("x"),
                TestToken::eof("parser-test", 1, 1, 1),
            ],
            index: 0,
        };
        let data = RecognizerData::new(
            "Mini.g4",
            Vocabulary::new([None, Some("'x'")], [None, Some("X")], [None::<&str>, None]),
        );
        let mut parser = BaseParser::new(CommonTokenStream::new(source), data);
        let matched = parser.match_token(1).expect("token 1 should match");

Found a 13 line (109 tokens) duplication in the following files:

  • Starting at line 13345 of src/parser.rs
  • Starting at line 15487 of src/parser.rs
    fn parser_matches_token_and_reports_mismatch() {
        let source = Source {
            tokens: vec![
                TestToken::new(1).with_text("x"),
                TestToken::eof("parser-test", 1, 1, 1),
            ],
            index: 0,
        };
        let data = RecognizerData::new(
            "Mini.g4",
            Vocabulary::new([None, Some("'x'")], [None, Some("X")], [None::<&str>, None]),
        );
        let mut parser = BaseParser::new(CommonTokenStream::new(source), data);

Found a 22 line (108 tokens) duplication in the following files:

  • Starting at line 6751 of src/parser.rs
  • Starting at line 7139 of src/parser.rs
    ) -> Option<RecognizeOutcome> {
        let (error_index, message) = self.expected_error_message(rule_index, start_index, expected);
        let diagnostic = diagnostic_for_token(self.token_at(error_index), message);
        let mut next_index = error_index;
        loop {
            let symbol = self.token_type_at(next_index);
            if sync_symbols.contains(&symbol) {
                if next_index == error_index {
                    return None;
                }
                break;
            }
            if symbol == TOKEN_EOF {
                break;
            }
            let after = self.consume_index(next_index, symbol);
            if after == next_index {
                break;
            }
            next_index = after;
        }
        let mut nodes = NodeSeqId::EMPTY;

Found a 15 line (108 tokens) duplication in the following files:

  • Starting at line 15741 of src/parser.rs
  • Starting at line 15765 of src/parser.rs
    fn outcome_ties_keep_later_non_recursive_alternative() {
        let arena = RecognitionArena::default();
        let first = RecognizeOutcome {
            index: 1,
            consumed_eof: false,
            alt_number: 0,
            member_values: BTreeMap::new(),
            return_values: BTreeMap::new(),
            diagnostics: DiagnosticSeqId::EMPTY,
            decisions: Vec::new(),
            actions: vec![ParserAction::new(1, 0, 0, None)],
            nodes: NodeSeqId::EMPTY,
        };
        let second = RecognizeOutcome {
            actions: vec![ParserAction::new(2, 0, 0, None)],

Found a 16 line (105 tokens) duplication in the following files:

  • Starting at line 5952 of src/parser.rs
  • Starting at line 6518 of src/parser.rs
        let start_state = atn.rule_to_start_state().get(rule_index).ok_or_else(|| {
            AntlrError::Unsupported(format!("rule {rule_index} has no start state"))
        })?;
        let stop_state = atn
            .rule_to_stop_state()
            .get(rule_index)
            .filter(|state| *state != usize::MAX)
            .ok_or_else(|| {
                AntlrError::Unsupported(format!("rule {rule_index} has no stop state"))
            })?;

        let start_index = self.current_visible_index();
        self.clear_prediction_diagnostics();
        self.reset_per_parse_caches();
        self.reset_recognition_arena();
        let caller_follow_state = self.pending_invoking_follow_state(atn);

Found a 13 line (100 tokens) duplication in the following files:

  • Starting at line 5687 of src/parser.rs
  • Starting at line 5711 of src/parser.rs
        let mut expected = BTreeSet::new();
        for index in (1..self.rule_context_stack.len()).rev() {
            let invoking_state = self.rule_context_stack[index].invoking_state;
            let Ok(state_number) = usize::try_from(invoking_state) else {
                continue;
            };
            let Some(Transition::Rule { follow_state, .. }) = atn
                .state(state_number)
                .and_then(|state| state.transitions().first())
                .map(ParserTransition::data)
            else {
                continue;
            };

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The fast recognizer now constructs parse-tree fragments in deferred arenas and materializes nodes after selecting the best outcome. Token, recovery, memoization, recursive-boundary, and rule-transition paths use deferred composition. Eligible * and + repetitions are traversed iteratively. Tests cover tree ordering, long repetitions, arena growth, deferred outcome fields, type sizes, and arena reset behavior.

Poem

I’m a rabbit with a parse-tree thread,
Deferred leaves hop where nodes once led.
Star loops dance, no stackpile grows,
Tokens bloom in ordered rows.
With every arena neatly reset,
Long-list parsing passes the test! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: making ordinary repetition stack-safe.
Description check ✅ Passed The description matches the changes by describing iterative loop handling and deferred parse-tree materialization.
Linked Issues check ✅ Passed The changes address #103 by iterating ordinary * and + loops, preventing stack growth on long valid repetitions.
Out of Scope Changes check ✅ Passed The rope/materialization work and repetition tests support the stated parser fix and are not unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tinovyatkin tinovyatkin changed the title [codex] Fix parser stack overflow in ordinary repetition Fix parser stack overflow in ordinary repetition Jul 17, 2026
@tinovyatkin
tinovyatkin marked this pull request as ready for review July 17, 2026 23:20

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a deferred-tree rope structure (using FastDeferredNode, FastDeferredRule, and related IDs) to optimize parser performance and memory usage during fast recognition, along with an iterative repetition walker (recognize_repetition_fast) to avoid deep native recursion on the call stack. The feedback highlights a stack-overflow risk in materialize_fast_deferred_nodes due to recursive processing of rule nodes on deep grammars, and suggests rewriting it using an iterative model with an explicit stack.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/parser.rs
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2817a43b96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/parser.rs
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parser.rs (1)

7594-7596: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Keep iterative repetition active during recovery.

Line 7594 disables the walker when recovery is enabled. A long repetition with a late syntax error fails the clean passes, then traverses the entire loop recursively during the recovery retry, reintroducing repetition-proportional stack growth and possible process abort.

Make coordinate merging recovery-aware and add a reduced-stack test with a malformed token after a long */+ sequence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser.rs` around lines 7594 - 7596, Update the fast repetition condition
around fast_repetition_shape so iterative repetition remains active during
recovery, making coordinate merging recovery-aware rather than disabling the
walker when fast recovery is enabled. Preserve correct recovery behavior, and
add a reduced-stack regression test covering a malformed token after a long * or
+ repetition sequence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/parser.rs`:
- Around line 7594-7596: Update the fast repetition condition around
fast_repetition_shape so iterative repetition remains active during recovery,
making coordinate merging recovery-aware rather than disabling the walker when
fast recovery is enabled. Preserve correct recovery behavior, and add a
reduced-stack regression test covering a malformed token after a long * or +
repetition sequence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d787a760-afd5-4eb4-8eae-b83ce9580a40

📥 Commits

Reviewing files that changed from the base of the PR and between 2817a43 and 36a4633.

📒 Files selected for processing (1)
  • src/parser.rs

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 36a4633d03

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

bug(parser): long (...)* star-loops recurse instead of iterate — 200-row INSERT overflows the stack (SIGABRT)

1 participant