Skip to content

Replace speculative Rc lists with an indexed arena - #89

Merged
tinovyatkin merged 3 commits into
mainfrom
codex/indexed-recognition-arena
Jul 16, 2026
Merged

Replace speculative Rc lists with an indexed arena#89
tinovyatkin merged 3 commits into
mainfrom
codex/indexed-recognition-arena

Conversation

@tinovyatkin

Copy link
Copy Markdown
Contributor

Addresses #83

What changed

  • Replace speculative RecognizedNode / FastRecognizedNode object graphs and persistent NodeList tails with compact node and sequence IDs into a parser-owned append-only arena.
  • Store terminal references as TokenId, and move missing-token text, return values, and diagnostics into side pools.
  • Keep memoized fast outcomes compact: cloning an outcome copies scalar fields and arena IDs rather than Rc node graphs.
  • Bulk-reset arena lengths between interpreted rule entries, retain bounded capacities for parser reuse, and drop pathological high-water allocations.
  • Expose RecognitionArenaStats for total/live/dead node, link, and extra counts plus retained capacities; emit the same counters through ANTLR_PERF_DUMP.
  • Document the measured interpreted-path timing, allocation, and RSS changes in the README.

Why

The previous persistent representation shared tails, but every speculative node and list tail still incurred allocator and reference-count traffic. Losing alternatives amplified that cost on ambiguous or deeply nested grammars. The arena turns node creation and prepend operations into append-only vector writes while preserving alternative ordering, diagnostics, left-recursive folding, semantic behavior, and child order.

The current public recursive tree is still produced at one compatibility boundary. Issue #84 will replace that boundary with direct flat-CST handoff; this PR does not add a second tree implementation or a legacy recognition fallback.

Measured impact

A same-machine forced-interpreter Kotlin comparison against c068688db used 100 measured parses per fixture:

Fixture Parse average Allocations / parse Allocated bytes / parse
lazy bodies 3.166 ms -> 2.498 ms (-21.1%) 44,842 -> 35,558 (-20.7%) 20.39 MB -> 18.85 MB (-7.5%)
coroutines flow 3.407 ms -> 2.544 ms (-25.3%) 45,005 -> 38,552 (-14.3%) 19.02 MB -> 18.00 MB (-5.3%)
Ktor route description 27.670 ms -> 16.686 ms (-39.7%) 355,148 -> 266,315 (-25.0%) 60.22 MB -> 45.82 MB (-23.9%)
Ktor security inference 14.577 ms -> 9.462 ms (-35.1%) 210,051 -> 164,095 (-21.9%) 38.75 MB -> 30.96 MB (-20.1%)

On the largest fixture, maximum RSS fell from 37.29 MB to 32.83 MB (-12.0%). The normal 17-fixture generated-parser sweep had no C#, Java, or Trino regression above 1%; Kotlin improved on all four fixtures.

Validation

  • cargo fmt --check
  • cargo test --locked --all-targets --all-features
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • cargo run --release --quiet --bin antlr4-runtime-testsuite (356 passed, 0 failed, 1 skipped)
  • tests/kotlin-parity/run.sh ... (nine file/script fixtures byte-identical to antlr4-python3-runtime)
  • Same-machine baseline/current interpreted allocation, timing, and RSS measurements
  • Same-machine 17-fixture Kotlin/C#/Java/Trino generated-parser timing sweep

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The parser now stores speculative nodes, child sequences, diagnostics, and related payloads in a parser-owned recognition arena addressed by compact IDs. Recognition, recovery, outcome selection, deduplication, boundary folding, and parse-tree materialization use arena helpers. RecognitionArenaStats reports record counts and retained capacities, with a public accessor and re-export. Tests, the changelog, and README document the arena behavior and benchmark results.

Poem

I’m a rabbit with a tidy new pen,
Arena nodes hop where vectors had been.
Diagnostics gather, IDs line the way,
Parse trees bloom at the end of the day.
Stats count the burrows—what a neat array! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: replacing speculative Rc-backed lists with an indexed arena.
Description check ✅ Passed The description matches the changeset and explains the arena refactor, stats exposure, and measured impact.

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.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Copy/Paste Detection

Found 10 duplication(s) across 2 changed Rust file(s) (threshold: 100 tokens).

Show duplications

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

  • Starting at line 12030 of src/parser.rs
  • Starting at line 12166 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 29 line (123 tokens) duplication in the following files:

  • Starting at line 7073 of src/parser.rs
  • Starting at line 7107 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,
                            },
                            visiting,
                            memo,
                            expected,
                        )
                        .into_iter()
                        .map(|mut outcome| {
                            if let Some(rule_index) = boundary {
                                let boundary = self.arena_boundary_node(rule_index);
                                self.arena_prepend(&mut outcome.nodes, boundary);
                            }
                            outcome
                        }),
                    );
                }

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

  • Starting at line 7960 of src/parser.rs
  • Starting at line 8033 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 21 line (113 tokens) duplication in the following files:

  • Starting at line 5542 of src/parser.rs
  • Starting at line 6068 of src/parser.rs
        let start_state = atn
            .rule_to_start_state()
            .get(rule_index)
            .copied()
            .ok_or_else(|| {
                AntlrError::Unsupported(format!("rule {rule_index} has no start state"))
            })?;
        let stop_state = atn
            .rule_to_stop_state()
            .get(rule_index)
            .copied()
            .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 15 line (113 tokens) duplication in the following files:

  • Starting at line 12091 of src/parser.rs
  • Starting at line 12374 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 14 line (110 tokens) duplication in the following files:

  • Starting at line 11846 of src/parser.rs
  • Starting at line 13608 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 11846 of src/parser.rs
  • Starting at line 13633 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 6300 of src/parser.rs
  • Starting at line 6670 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 13863 of src/parser.rs
  • Starting at line 13887 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 12 line (102 tokens) duplication in the following files:

  • Starting at line 11387 of src/parser.rs
  • Starting at line 11427 of src/parser.rs
        atn.add_state(AtnState::new(4, AtnStateKind::BlockEnd).with_rule_index(0));
        atn.add_state(AtnState::new(5, AtnStateKind::RuleStop).with_rule_index(0));
        atn.set_rule_to_start_state(vec![0]);
        atn.set_rule_to_stop_state(vec![5]);
        atn.add_decision_state(1);
        atn.state_mut(0)
            .expect("state 0")
            .add_transition(Transition::Epsilon { target: 1 });
        atn.state_mut(1)
            .expect("state 1")
            .add_transition(Transition::Atom {
                target: 2,

@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 parser-owned, index-addressed recognition arena to replace speculative Rc node lists with compact IDs, which significantly improves parsing performance and reduces memory allocations. It also exports RecognitionArenaStats and updates the changelog and README with detailed benchmark results. There are no review comments, so I have no feedback to provide.

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.

@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 `@src/parser.rs`:
- Line 5642: The arena statistics snapshots in src/parser.rs at lines 5642-5642
and 6165-6165 are taken before recursive tree materialization and boundary
folding completes. Move each record_recognition_arena_stats call to after all
folding/materialization, passing the final folded root’s arena state so
RecognitionArenaStats reflects current totals, live records, and capacities.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e58a496f-2eb6-4f9d-836d-5d685ccf2095

📥 Commits

Reviewing files that changed from the base of the PR and between c068688 and 8b364ac.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • src/lib.rs
  • src/parser.rs

Comment thread src/parser.rs Outdated

@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: 8b364ac532

ℹ️ 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 Outdated
@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: fbf3b1c2db

ℹ️ 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 Outdated
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: c0d4ebbb6a

ℹ️ 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".

@tinovyatkin tinovyatkin changed the title [codex] Replace speculative Rc lists with an indexed arena Replace speculative Rc lists with an indexed arena Jul 16, 2026
@tinovyatkin
tinovyatkin merged commit 3bf4c59 into main Jul 16, 2026
11 checks passed
@tinovyatkin
tinovyatkin deleted the codex/indexed-recognition-arena branch July 16, 2026 07:12
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.

1 participant