Skip to content

fix(codegen): guard generated rule dispatch against native stack overflow - #194

Merged
tinovyatkin merged 1 commit into
mainfrom
fix/193-generated-rule-stack-guard
Jul 25, 2026
Merged

fix(codegen): guard generated rule dispatch against native stack overflow#194
tinovyatkin merged 1 commit into
mainfrom
fix/193-generated-rule-stack-guard

Conversation

@tinovyatkin

@tinovyatkin tinovyatkin commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #193.

What

Generated recursive-descent rule methods mapped grammar-rule nesting directly onto native call depth — the CEL grammar walks ~9 rules per [, so ~850 nesting levels aborted the process on an 8 MiB stack. The interpreted path (recognize_state_fast, #147) and the tree walker were already segmented-stack safe; the generated parse path was the last unguarded recursion.

The generator now emits a stack-capacity probe at the shared parse_generated_rule_N_dispatch boundary:

if self.base.generated_rule_stack_check_due() {
    antlr4_runtime::grow_generated_rule_stack(|| self.parse_generated_rule_N(...))
} else {
    self.parse_generated_rule_N(...)
}

generated_rule_stack_check_due samples once per 8 rule-context frames (rule_context_stack.len() % 8 == 0 — no new state), and grow_generated_rule_stack re-uses recognize_state_fast's red-zone constants (1 MiB red zone, 4 MiB segments) via stacker::maybe_grow.

Verification

  • New e2e test: deep-nesting/Nest.g4 (CEL-shaped 6-rule chain) parses 10 000 nesting levels on the default 2 MiB test-thread stack. Before the fix that input aborts the process.

  • CEL spike: the exact bug(codegen): generated recursive-descent rules overflow the native stack on deeply nested input #193 reproducer now parses at depth 100 000 (previously aborted at ~850).

  • Conformance: full sweep 357 passed, 0 failed, 0 skipped.

  • Perf: Kotlin-parity dumper, min of 3×30 iters, quiet machine, baseline = origin/main runtime + codegen:

    snippet pre-fix guarded
    01-nested-types 0.105 ms 0.106 ms
    02-dataframe 0.767 ms 0.774 ms
    03-string-templates 0.321 ms 0.317 ms

    Within noise (±1%). Parse trees byte-identical pre/post on all snippets.

  • cargo test --locked (unit + doctests) and cargo clippy --locked --all-targets --all-features -- -D warnings clean.

Summary by CodeRabbit

  • Bug Fixes

    • Improved parsing of deeply nested input to prevent native stack overflows.
    • Added safer handling for recursive parser calls.
  • Tests

    • Added end-to-end coverage for parsing input nested 10,000 levels deep.
    • Added a grammar fixture covering nested expressions, unary operators, and whitespace handling.

…flow

Generated recursive-descent rule methods mapped grammar-rule nesting
directly onto native call depth: the CEL grammar walks ~9 rules per `[`,
so ~850 nesting levels aborted the process on an 8 MiB stack while the
interpreted path (recognize_state_fast, #147) and the tree walker were
already segmented-stack safe.

Emit a capacity probe at the shared parse_generated_rule_N_dispatch
boundary, sampled once per 8 rule-context frames, growing onto a
segmented stack via the same red-zone constants recognize_state_fast
uses. Deeply nested input now parses (or reports a syntax error)
instead of aborting.

Fixes #193
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Generated recursive rule dispatch now checks stack capacity and invokes a stack-growth wrapper when sampling indicates it is due. The runtime helper is publicly exported, and an end-to-end test validates generated parsing of 10,000 nested brackets.

Changes

Generated parser stack growth

Layer / File(s) Summary
Runtime stack-growth support
src/parser.rs, src/lib.rs
Adds the grow_generated_rule_stack wrapper, periodic stack-check method, and public re-export.
Generated dispatch stack guard
src/bin/antlr4-rust-gen.rs
Wraps eligible generated rule calls with stack growth while preserving left-recursive precedence dispatch.
Deep-nesting generator regression test
tests/fixtures/antlr4-rust-gen/deep-nesting/Nest.g4, tests/antlr4_rust_gen_cli.rs
Adds a recursive grammar and verifies generated parsing of 10,000 nested brackets. 

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant antlr4-rust-gen
  participant GeneratedParser
  participant BaseParser
  participant StackGrowth
  Test->>antlr4-rust-gen: generate parser from Nest.g4
  antlr4-rust-gen-->>Test: generated parser with stack guard
  Test->>GeneratedParser: parse 10,000 nested brackets
  GeneratedParser->>BaseParser: check generated_rule_stack_check_due()
  BaseParser-->>GeneratedParser: stack check result
  GeneratedParser->>StackGrowth: grow stack around rule call when due
  StackGrowth-->>GeneratedParser: return parsed rule result
  GeneratedParser-->>Test: parse tree with rule node
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: guarding generated rule dispatch against native stack overflow.
Linked Issues check ✅ Passed The PR adds stack-growth guarding, a public wrapper, and a 10,000-depth regression test, satisfying #193's core requirements.
Out of Scope Changes check ✅ Passed The parser helper, generator change, fixture, and regression test all support the stack-overflow fix and stay within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/193-generated-rule-stack-guard

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.

@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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@tests/antlr4_rust_gen_cli.rs`:
- Around line 1572-1578: Strengthen the assertion in the generated parser test
to count dispatch method declarations and require a nonzero count of
`antlr4_runtime::grow_generated_rule_stack` calls equal to that declaration
count. Replace the single `parser.contains` check while preserving the existing
failure context and generated-parser validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7968553f-4362-4c54-abda-ca71a5712986

📥 Commits

Reviewing files that changed from the base of the PR and between 6eeb334 and 65d427e.

📒 Files selected for processing (5)
  • src/bin/antlr4-rust-gen.rs
  • src/lib.rs
  • src/parser.rs
  • tests/antlr4_rust_gen_cli.rs
  • tests/fixtures/antlr4-rust-gen/deep-nesting/Nest.g4

Comment thread tests/antlr4_rust_gen_cli.rs
@tinovyatkin
tinovyatkin merged commit 3634641 into main Jul 25, 2026
8 of 11 checks passed
@tinovyatkin
tinovyatkin deleted the fix/193-generated-rule-stack-guard branch July 25, 2026 12:57
@tinovyatkin
tinovyatkin restored the fix/193-generated-rule-stack-guard branch July 25, 2026 13:11
@github-actions

Copy link
Copy Markdown

📊 Source Code Metrics (this PR vs main)

No metric changes detected.

Generated by mehen v1.7.0 — the code quality watcher.

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 42.85714% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/parser.rs 0.00% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown

Copy/Paste Detection

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

Show duplications

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

  • Starting at line 15391 of src/parser.rs
  • Starting at line 15524 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,
            },
        ];
```rust

---

Found a 22 line (132 tokens) duplication in the following files:
* Starting at line 1100 of tests/antlr4_rust_gen_cli.rs
* Starting at line 1200 of tests/antlr4_rust_gen_cli.rs

```rust
        "parser grammar Delegate;\ndelegated: {isTypeName()}? ID;\n",
    )
    .expect("delegate grammar should be writable");
    fs::write(&tokens, "lexer grammar Tokens;\nID: [a-z]+;\n")
        .expect("token grammar should be writable");

    let output = run_antlr4_rust_gen(&[
        root.as_os_str(),
        tokens.as_os_str(),
        OsStr::new("-I"),
        temp.path().as_os_str(),
        OsStr::new("--out-dir"),
        out.as_os_str(),
    ]);
    assert!(
        output.status.success(),
        "stdout: {}\nstderr: {}",
        utf8(&output.stdout),
        utf8(&output.stderr)
    );
    let parser = fs::read_to_string(out.join("root.rs")).expect("parser should be emitted");
    assert!(parser.contains("pub trait RootHooks"), "{parser}");

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

  • Starting at line 14349 of src/parser.rs
  • Starting at line 14421 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))
```rust

---

Found a 22 line (125 tokens) duplication in the following files:
* Starting at line 14375 of src/parser.rs
* Starting at line 14447 of src/parser.rs

```rust
            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 34 line (119 tokens) duplication in the following files:

  • Starting at line 9783 of src/parser.rs
  • Starting at line 9858 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,
                                    committed_decision: transition_committed,
                                    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);
```rust

---

Found a 15 line (113 tokens) duplication in the following files:
* Starting at line 15449 of src/parser.rs
* Starting at line 15725 of src/parser.rs

```rust
    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 12 line (112 tokens) duplication in the following files:

  • Starting at line 13330 of src/parser.rs
  • Starting at line 13413 of src/parser.rs
        let mut atn = ParserAtnBuilder::new(1);
        for (state, kind, rule) in [
            (0, AtnStateKind::RuleStart, 0),
            (1, AtnStateKind::StarLoopEntry, 0),
            (2, AtnStateKind::Basic, 0), // ops hub
            (3, AtnStateKind::Basic, 0), // shift prec
            (4, AtnStateKind::Basic, 0), // shift first >
            (5, AtnStateKind::Basic, 0), // shift second >
            (6, AtnStateKind::Basic, 0), // rel prec
            (7, AtnStateKind::Basic, 0), // rel >
            (8, AtnStateKind::LoopEnd, 0),
            (9, AtnStateKind::RuleStop, 0),
```rust

---

Found a 22 line (112 tokens) duplication in the following files:
* Starting at line 14786 of src/parser.rs
* Starting at line 14987 of src/parser.rs

```rust
    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 13152 of src/parser.rs
  • Starting at line 14786 of src/parser.rs
  • Starting at line 14987 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))
```rust

---

Found a 14 line (110 tokens) duplication in the following files:
* Starting at line 15120 of src/parser.rs
* Starting at line 17876 of src/parser.rs

```rust
    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 15120 of src/parser.rs
  • Starting at line 17901 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);
```rust

---

Found a 22 line (108 tokens) duplication in the following files:
* Starting at line 7578 of src/parser.rs
* Starting at line 7969 of src/parser.rs

```rust
    ) -> 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 18167 of src/parser.rs
  • Starting at line 18191 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)],
```rust

---

Found a 16 line (105 tokens) duplication in the following files:
* Starting at line 6717 of src/parser.rs
* Starting at line 7337 of src/parser.rs

```rust
        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 6445 of src/parser.rs
  • Starting at line 6469 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;
            };
```rust

</details>

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @tinovyatkin's task in 14m 18s —— View job


Code review — PR #194

No blocking issues found. Checked for bugs and CLAUDE.md compliance across the full diff (5 files, +140/−13). Reviewed post-merge against 6eeb334 (base) → 65d427e (head).

  • Gather context (diff vs origin/main, changed files)
  • Review runtime helper (src/parser.rs, src/lib.rs)
  • Review generator emission (src/bin/antlr4-rust-gen.rs)
  • Review test + fixture (tests/antlr4_rust_gen_cli.rs, Nest.g4)
  • Cross-check against upstream ANTLR v4.13.2 semantics
  • Post consolidated review

What was verified

The core claim — that the guard actually closes the hole — was validated empirically rather than by inspection:

Hypothesis Result
Guard covers every recursion path Holds. Every emission site that generates a nested-rule call routes through _dispatch (antlr4-rust-gen.rs:5189, :5195, :5826, :5842). Alternatives are inlined into the rule body — there are no helper methods that could recurse around the guard. Generated a left-recursive grammar to confirm _precedence is only ever reached via _dispatch.
rule_context_stack.len() tracks native depth Holds. Exactly one push (parser.rs:5723) and one pop (:5755); every generated rule calls enter_rule/enter_recursion_rule. Depth moves ±1 per frame, so no multiple of 8 is ever skipped — the modulo sampling is sound rather than merely probable. The check reads the counter before the callee's enter_rule, so it samples the caller's depth.
Test is a real regression test Confirmed by neutering the guard (if false && … at all 7 dispatch sites) and re-running: fatal runtime error: stack overflow, aborting (SIGABRT). As generated: passes in 0.23 s. Measured start-of-test remaining stack 2,033,152 bytes, matching the test's 2 MiB comment. The generated path is taken by default here — no fallback masks the fix.
Between-sample growth stays under the red zone ~3.6 KB per rule frame in a debug build (worst case) → ~29 KB per 8-frame window against a 1 MiB red zone, a ~36× margin.
Nested maybe_grow / unwinding No hazard. Reuses the identical constants and shape as three pre-existing sites (parser.rs:8375, tree.rs:1330, tree_pattern.rs:861). Result propagates normally across segment boundaries.
Feature-gating stacker = "0.1.24" is an unconditional dependency (Cargo.toml:36), so the new unconditional pub fn cannot break a no-default-features build. grow_generated_rule_stack leaks no stacker types.
CLAUDE.md compliance Clean. cargo fmt --check and cargo clippy --locked --all-targets --all-features -- -D warnings both pass. The insta rule isn't triggered (no insta macro added; .contains() on generated source is the established convention throughout antlr4_rust_gen_cli.rs). .clippy.toml bans Option::unwrap/Result::unwrap — the new test uses only .expect(…), matching neighbours. No AGENTS.md/CLAUDE.md section added, so the sync rule doesn't apply.

The emitted code expands correctly through the \-continuations and {{/}} escapes at antlr4-rust-gen.rs:5228-5234 — braces balance, the if/else is the tail expression, and both arms yield the same Result. Interpolating target_call twice is inert (no braces in the string, only one arm executes). let _ = precedence; preceding a use of precedence is fine — _ is a wildcard pattern that neither binds nor moves.

Non-blocking observations

1. Deep nesting now trends toward OOM rather than abort. The generated path has no ceiling analogous to RECOGNITION_DEPTH_LIMIT (parser.rs:105), so segment allocation is unbounded — the 10 000-level test peaks at ~220 MB RSS in a debug build. This is the intended trade and strictly better than SIGSEGV, but for untrusted input the failure mode moved from "abort" to "allocate until the allocator says no". Worth a follow-up if untrusted input is in scope. Add an opt-in depth ceiling →

2. Test assertion is weaker than its comment (also raised by CodeRabbit). The comment says "Every generated dispatch method must carry the stack guard" but contains proves only one occurrence. Since the generator emits the guard from a single unconditional code path (antlr4-rust-gen.rs:5228), the weaker check still catches a full regression — so this is a wording/strength gap, not a correctness bug. Cheap to tighten anyway. Fix this →

3. Doc-comment phrasing. parser.rs:111-116 states the sampling guarantees "a check runs before the red zone can be crossed". Strictly, that's a conclusion resting on the per-frame size assumption (8 frames ≪ 1 MiB), not something the code enforces. The measured 36× margin makes it true in practice; the wording just reads as stronger than it is. No change needed.

Upstream parity

Nothing to flag. ANTLR's Java runtime maps rule nesting onto JVM call depth and simply throws StackOverflowError, so there is no upstream behaviour to diverge from here — this is a Rust-specific robustness addition, consistent with the crate's existing segmented-stack treatment of recognize_state_fast (#147), the tree walker, and the tree-pattern matcher.


Reviewed by 4 independent agents (2× CLAUDE.md compliance, 2× bug/logic); every candidate finding was empirically validated or dropped.
· branch fix/193-generated-rule-stack-guard

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(codegen): generated recursive-descent rules overflow the native stack on deeply nested input

1 participant