Skip to content

[codex] Optimize C# parse benchmark runtime - #8

Merged
tinovyatkin merged 6 commits into
mainfrom
tino/csharp-runtime-benchmark-optimizations
May 23, 2026
Merged

[codex] Optimize C# parse benchmark runtime#8
tinovyatkin merged 6 commits into
mainfrom
tino/csharp-runtime-benchmark-optimizations

Conversation

@tinovyatkin

@tinovyatkin tinovyatkin commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a direct cached lexer DFA replay path and visible-token jump cache to reduce repeated lexer/token-stream work on large parse-benchmark fixtures.
  • Split parser recognition into a clean fast pass and recovery retry, avoiding recovery bookkeeping, terminal node allocation, and visiting-set churn on valid inputs.
  • Compact parser FIRST/lookahead sets and defer speculative parse-tree node materialization until an accepted path needs it.

Impact

The C# Rust benchmark mean is now around the target 200 ms while preserving runtime conformance. The largest C# fixture remains slower than Go, but the Rust runtime is much closer than the prior multi-second baseline.

10-iteration C# comparison:

csharp/dotnet-wpf-datagrid-column.cs  rust-antlr 87.107ms   go-antlr 18.856ms
csharp/mono-codegen.cs                rust-antlr 82.427ms   go-antlr 36.889ms
csharp/mono-anonymous.cs              rust-antlr 150.799ms  go-antlr 39.036ms
csharp/mono-statement.cs              rust-antlr 493.657ms  go-antlr 134.402ms

Rust mean across the four C# fixtures: 203.5 ms.

Kotlin quick benchmark remains healthy, with Rust still faster than Go on the checked Kotlin fixtures.

Validation

  • git diff --check
  • cargo test --quiet
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • python3 tools/parse-bench/run.py --languages csharp --runtimes rust-antlr,go-antlr --iters 10 --warmups 2 --json target/parse-bench/csharp-rust-go-final-10iters.json --markdown target/parse-bench/csharp-rust-go-final-10iters.md
  • python3 tools/parse-bench/run.py --quick --languages kotlin --runtimes rust-antlr,go-antlr --json target/parse-bench/kotlin-rust-go-final-quick.json --markdown target/parse-bench/kotlin-rust-go-final-quick.md
  • cargo run --release --quiet --bin antlr4-runtime-testsuite -> summary: 357 passed, 0 failed, 0 skipped, 357 run

Summary by CodeRabbit

  • Refactor
    • Optimized lexer performance with improved caching mechanisms.
    • Enhanced parser recognition efficiency through token-stream improvements.
    • Improved token visibility tracking with caching.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@tinovyatkin, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 35 minutes and 52 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0900bf89-f87b-4452-adb4-80e883088e6d

📥 Commits

Reviewing files that changed from the base of the PR and between c483bac and 0404e07.

📒 Files selected for processing (2)
  • src/atn/lexer.rs
  • src/parser.rs
📝 Walkthrough

Walkthrough

This PR optimizes the ANTLR Rust runtime lexer, parser, and token stream. The lexer now caches DFA states with relative-position keying. The parser's fast recognizer is refactored with bitset-backed token sets, persistent node structures, and conditional recovery logic. The token stream caches visibility relationships between buffered tokens.

Changes

Lexer DFA Caching and Parser Fast Recognizer Optimization

Layer / File(s) Summary
Lexer DFA caching data structures and accessors
src/lexer.rs
LexerDfaActionKey encodes relative action positions; LexerDfaConfigKey now stores action keys instead of plain indices. New cached structures (LexerDfaCachedTransition, LexerDfaCachedAccept, LexerDfaCachedState) capture transition targets, accept metadata, and per-state caching. BaseLexer exposes pub(crate) accessors to read/write these cached entries.
Lexer cached token matching pipeline
src/atn/lexer.rs
next_token routes through next_token_with_cache wrapper. Hook execution is factored into next_token_with_hooks_impl with a use_cache flag. New match_token_cached pipeline retrieves cached DFA states, precomputed accept info, and cached transitions; on misses, it falls back to full ATN expansion and materializes cache entries. DFA keying is refactored to incorporate token_start, making cache identity stable across absolute input positions via relative action deltas.
Parser token set bitset representation and FIRST computation
src/parser.rs
TokenBitSet provides compact token membership/union via bitwise operations. FirstSet and TransitionLookSet replace BTreeSet<i32> with TokenBitSet fields. transition_first_set and rule_first_set_inner are rewritten to populate/merge bitsets instead of iterating BTreeSet entries.
Parser pruning logic and expected-token diagnostics
src/parser.rs
should_skip_via_lookahead gains record_expected parameter to control diagnostic contribution. New helpers should_skip_rule_via_first_set and record_token_bit_expected perform bitset-aware rule pruning and expected-token recording.
Parser memoization key refactoring and recovery control
src/parser.rs
FAST_RECOGNIZER_DEFERRED_FILL_AT constant controls token stream eager filling threshold. BaseParser gains fast_recovery_enabled and fast_token_nodes_enabled flags. FastRecognizeKey replaces Rc<BTreeSet<i32>> with interned recovery_symbols_id: usize to reduce key size; equality/hashing are updated accordingly. dedupe_clean_fast_outcomes deduplicates outcomes by (index, consumed_eof) when recovery is disabled. Flags are toggled during parse attempts to probe first without recovery.
Fast recognizer persistent node structure
src/parser.rs
NodeList and NodeListIter provide persistent ordered traversal of parse-tree children. FastRecognizedNode::Rule stores children: NodeList. fast_recognized_node_tree folds left-recursive boundaries when present. fast_recognized_node_tree_with_implicit_tokens reconstructs implicit terminals. fast_recognized_node_span computes node start/end token indices.
Fast recognition state machine with conditional recovery
src/parser.rs
recognize_state_fast constructs memo keys with interned recovery symbols, conditionally invokes fast recovery context, gates lookahead pruning on fast_recovery_enabled, snapshots expected-tokens for rule processing, renders rule outcomes with NodeList children, conditionally prepends terminal nodes when fast_token_nodes_enabled is enabled, handles mismatches with recovery-gated diagnostics, explores single-token repairs, and selects deduplication strategy based on recovery setting.
Parser utility functions and optimizations
src/parser.rs
Streamlines FxHasher::write formatting, precomputes memo capacity in fast_recognize_top, defers token stream filling in token_type_at beyond a threshold, and documents intern_recovery_symbols pointer-identity invariants.
Token stream visibility caching
src/token_stream.rs
CommonTokenStream maintains per-index cache next_visible_after initialized with UNKNOWN_NEXT_VISIBLE sentinels. Appends cache entries during token buffering. next_visible_after consults cache first; on miss, scans forward, stores result, and returns it. New is_filled() exposes whether EOF has been buffered.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 With caches deep and bitsets true,
The lexer flies, the parser's new—
DFA keys that shift with grace,
Nodes persist in their own place,
Token streams know what comes next—
A runtime swift, no longer vexed!

🚥 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 '[codex] Optimize C# parse benchmark runtime' accurately describes the main objective: improving parser/lexer performance for C# benchmarks through caching and optimization strategies.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tino/csharp-runtime-benchmark-optimizations

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 and usage tips.

@github-actions

github-actions Bot commented May 23, 2026

Copy link
Copy Markdown

Copy/Paste Detection

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

Show duplications

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

  • Starting at line 193 of src/atn/lexer.rs
  • Starting at line 217 of src/atn/lexer.rs
pub fn next_token_with_hooks<I, F, A, P, E>(
    lexer: &mut BaseLexer<I, F>,
    atn: &Atn,
    mut custom_action: A,
    mut semantic_predicate: P,
    mut accept_adjuster: E,
) -> CommonToken
where
    I: CharStream,
    F: TokenFactory,
    A: FnMut(&mut BaseLexer<I, F>, LexerCustomAction),
    P: FnMut(&BaseLexer<I, F>, LexerPredicate) -> bool,
    E: FnMut(&mut BaseLexer<I, F>, i32, usize),
{
    next_token_with_hooks_impl(
        lexer,
        atn,
        &mut custom_action,
        &mut semantic_predicate,
        &mut accept_adjuster,

Found a 31 line (121 tokens) duplication in the following files:

  • Starting at line 2811 of src/parser.rs
  • Starting at line 2846 of src/parser.rs
                | Transition::Action { target, .. } => {
                    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 {
                                outcome.nodes.prepend(Rc::new(
                                    FastRecognizedNode::LeftRecursiveBoundary { rule_index },
                                ));
                            }
                            outcome
                        }),
                    );
                }

Found a 20 line (120 tokens) duplication in the following files:

  • Starting at line 420 of src/atn/lexer.rs
  • Starting at line 535 of src/atn/lexer.rs
            let Some(state) = atn.state(config.state) else {
                continue;
            };
            for transition in &state.transitions {
                if !transition.matches(symbol, MIN_CHAR_VALUE, MAX_CHAR_VALUE) {
                    continue;
                }
                let mut advanced = config.clone();
                set_config_state(atn, &mut advanced, transition.target());
                if symbol == EOF {
                    advanced.consumed_eof = true;
                } else {
                    advanced.position += 1;
                }
                next.push(advanced);
            }
        }

        let closure = epsilon_closure(lexer, atn, next, semantic_predicate);
        let target_has_semantic_context = closure.has_semantic_context;

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

  • Starting at line 2733 of src/parser.rs
  • Starting at line 3522 of src/parser.rs
            recovery_symbols_id: Rc::as_ptr(&recovery_symbols) as usize,
            recovery_state,
        };
        if let Some(outcomes) = memo.get(&key) {
            return outcomes.clone();
        }

        let visit_key = key.clone();
        if !visiting.insert(visit_key.clone()) {
            return Vec::new();
        }

        let Some(state) = atn.state(state_number) else {
            visiting.remove(&visit_key);
            return Vec::new();
        };
        let next_decision_start_index = if starts_prediction_decision(state) {
            Some(index)
        } else {
            decision_start_index
        };
        let (epsilon_recovery_symbols, epsilon_recovery_state) = if self.fast_recovery_enabled {

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

  • Starting at line 5451 of src/parser.rs
  • Starting at line 5615 of src/parser.rs
    fn parser_matches_token_and_reports_mismatch() {
        let source = Source {
            tokens: vec![
                CommonToken::new(1).with_text("x"),
                CommonToken::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);
        assert_eq!(
            parser.match_token(1).expect("token 1 should match").text(),

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

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

  • Starting at line 5659 of src/parser.rs
  • Starting at line 5682 of src/parser.rs
    fn outcome_ties_keep_later_non_recursive_alternative() {
        let first = RecognizeOutcome {
            index: 1,
            consumed_eof: false,
            alt_number: 0,
            member_values: BTreeMap::new(),
            return_values: BTreeMap::new(),
            diagnostics: Vec::new(),
            decisions: Vec::new(),
            actions: vec![ParserAction::new(1, 0, 0, None)],
            nodes: vec![RecognizedNode::Token { index: 0 }],
        };
        let second = RecognizeOutcome {
            actions: vec![ParserAction::new(2, 0, 0, None)],

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

  • Starting at line 5451 of src/parser.rs
  • Starting at line 5590 of src/parser.rs
  • Starting at line 5615 of src/parser.rs
    fn parser_matches_token_and_reports_mismatch() {
        let source = Source {
            tokens: vec![
                CommonToken::new(1).with_text("x"),
                CommonToken::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 19 line (106 tokens) duplication in the following files:

  • Starting at line 1706 of src/parser.rs
  • Starting at line 2138 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();

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

  • Starting at line 1898 of src/parser.rs
  • Starting at line 1937 of src/parser.rs
            FastRecognizedNode::Rule {
                rule_index,
                invoking_state,
                start_index,
                stop_index,
                children,
            } => {
                let mut context = ParserRuleContext::new(*rule_index, *invoking_state);
                if let Some(token) = self.token_at(*start_index) {
                    context.set_start(token);
                }
                if let Some(token) = stop_index.and_then(|index| self.token_at(index)) {
                    context.set_stop(token);
                }
                if children.has_left_recursive_boundary() {
                    let folded = fold_fast_left_recursive_boundaries(children.to_vec());

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

  • Starting at line 1883 of src/parser.rs
  • Starting at line 4583 of src/parser.rs
            FastRecognizedNode::MissingToken {
                token_type,
                at_index,
                text,
            } => {
                let current = self.token_at(*at_index);
                let token = CommonToken::new(*token_type)
                    .with_text(text)
                    .with_span(usize::MAX, usize::MAX)
                    .with_position(
                        current.as_ref().map(Token::line).unwrap_or_default(),
                        current.as_ref().map(Token::column).unwrap_or_default(),
                    );
                Ok(ParseTree::Error(ErrorNode::new(token)))
            }

@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 significant performance optimizations to the lexer and parser, including a lexer DFA cache, a bitset-based FIRST set implementation, and a lookahead cache in the token stream. Feedback highlights a critical issue where lexer action positions are lost during caching, which could break position-dependent logic. Reviewers also suggested optimizing bitset range operations, avoiding eager tokenization of the entire input to preserve lazy-lexing, and using ordered collections in the fast recognizer to ensure deterministic behavior and stable traces.

Comment thread src/atn/lexer.rs
Comment on lines +670 to +679
actions: accept
.actions
.iter()
.map(|action| LexerActionTrace {
action_index: action.action_index,
position: token_start,
rule_index: action.rule_index,
})
.collect(),
}

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.

high

The cached accept state logic currently loses the precise input position of lexer actions. In close_config, LexerActionTrace records the exact config.position where an action transition was reached. However, LexerDfaCachedAccept only stores the position_delta for the final accept boundary, and cached_accept_state applies the same token_start to all actions. This will break lexer actions that rely on Text() or other position-dependent logic during replay. LexerDfaActionKey should be updated to store its own position_delta relative to the token start to ensure stable traces and correct action execution.

References
  1. In performance-critical hot paths, use structured sortable types for cache keys instead of string formatting and joining to avoid unnecessary allocations and ensure stable traces.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7639fca. LexerDfaActionKey now carries a per-action position_delta relative to the token start, and cached configs/accept states reconstruct LexerActionTrace.position as token_start + position_delta instead of collapsing every action to the token start. I also passed the token start through the DFA key/cache conversion path so replayed configs keep stable position-sensitive action traces. Validated with cargo test --quiet, strict clippy, and targeted runtime descriptor LexerExec/PositionAdjustingLexer.

Comment thread src/parser.rs
Comment on lines +637 to +646
fn extend_range(&mut self, start: i32, stop: i32) {
let (start, stop) = if start <= stop {
(start, stop)
} else {
(stop, start)
};
for symbol in start..=stop {
self.insert(symbol);
}
}

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.

medium

The extend_range implementation for TokenBitSet is suboptimal for large ranges (like Wildcard transitions) because it iterates through every symbol and calls insert, which includes redundant bounds checks and potential Vec resizes. Since this is a bitset, you can optimize this using bit-parallel operations to fill entire words at once.

    fn extend_range(&mut self, start: i32, stop: i32) {
        let (start, stop) = if start <= stop { (start, stop) } else { (stop, start) };
        let start_slot = if start == TOKEN_EOF { 0 } else if start > 0 { start as usize } else if stop >= TOKEN_EOF { 0 } else if stop > 0 { 1 } else { return; };
        let stop_slot = if stop == TOKEN_EOF { 0 } else if stop > 0 { stop as usize } else { return; };
        if start_slot > stop_slot { return; }

        let start_word = start_slot / 64;
        let end_word = stop_slot / 64;
        if end_word >= self.words.len() {
            self.words.resize(end_word + 1, 0);
        }

        if start_word == end_word {
            let mask = (!0u64 << (start_slot % 64)) & (!0u64 >> (63 - (stop_slot % 64)));
            self.words[start_word] |= mask;
        } else {
            self.words[start_word] |= !0u64 << (start_slot % 64);
            for i in (start_word + 1)..end_word {
                self.words[i] = !0u64;
            }
            self.words[end_word] |= !0u64 >> (63 - (stop_slot % 64));
        }
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7639fca. TokenBitSet::extend_range now fills whole u64 words through extend_slot_range instead of looping through every symbol and repeatedly calling insert. EOF is handled separately before the positive token range is mapped to bit slots. Validated with cargo test --quiet, strict clippy, and the C# parse benchmark rerun.

Comment thread src/parser.rs Outdated
})?;

let start_index = self.current_visible_index();
self.input.fill();

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.

medium

Calling self.input.fill() at the start of every rule parse forces the lexer to tokenize the entire input stream immediately. While this might improve benchmark stability by removing lexing overhead from the measurement, it breaks the lazy-lexing design of CommonTokenStream. For very large files, this could lead to significant latency spikes or memory exhaustion before parsing even begins. Consider if this can be made optional or deferred.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7639fca. The up-front self.input.fill() in parse_atn_rule is removed, so rule parsing no longer forces complete tokenization before parsing starts. To avoid regressing large valid parses into per-token sync overhead, token_type_at now performs a deferred fill only after the fast recognizer has actually advanced into the stream (index >= 64). This keeps startup lazy while preserving the benchmark behavior for large files. Validated with cargo test --quiet, strict clippy, targeted runtime descriptors, and the C# benchmark rerun.

Comment thread src/parser.rs Outdated
Comment on lines +1779 to +1781
let memo_capacity = self.input.size().saturating_mul(4).min(1_000_000);
let mut visiting = FxHashSet::with_capacity_and_hasher(256, FxBuildHasher::default());
let mut memo = FxHashMap::with_capacity_and_hasher(memo_capacity, FxBuildHasher::default());

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.

medium

The memo_capacity heuristic is quite aggressive and can lead to high memory usage. Furthermore, to ensure a deterministic state index and preserve stable traces during parsing, use BTreeMap and BTreeSet instead of hash-based collections.

Suggested change
let memo_capacity = self.input.size().saturating_mul(4).min(1_000_000);
let mut visiting = FxHashSet::with_capacity_and_hasher(256, FxBuildHasher::default());
let mut memo = FxHashMap::with_capacity_and_hasher(memo_capacity, FxBuildHasher::default());
let mut visiting = BTreeSet::new();
let mut memo = BTreeMap::new();
References
  1. Use ordered maps (e.g., BTreeMap) instead of hash maps when a deterministic state index is required to preserve stable behavior or traces.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Partially fixed in 7639fca. I reduced the aggressive sizing by removing the eager full-input fill that made input.size() equal to the whole token stream before the first pass, and capped the initial memo reserve at 262_144 instead of 1_000_000. I intentionally kept FxHashMap/FxHashSet for the fast recognizer memo and visiting sets: these caches are parser-internal, not externally iterated, and do not define DFA state numbering or emitted traces. Switching this hot path to BTreeMap/BTreeSet would trade away the benchmark improvement without adding observable determinism; the externally visible lexer DFA trace storage remains ordered.

@tinovyatkin
tinovyatkin marked this pull request as ready for review May 23, 2026 14:47
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 7639fca2cf

ℹ️ 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
Comment on lines +2730 to +2736
let inserted_visit = if self.fast_recovery_enabled {
if !visiting.insert(visit_key.clone()) {
return Vec::new();
}
true
} else {
false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep cycle guard active in fast first-pass recognition

Disabling visiting whenever fast_recovery_enabled is false allows recognize_state_fast to recurse back into the same (state_number, stop_state, index, ...) key through epsilon/nullable cycles during the first pass, because the only remaining brake is the depth limit. Combined with single-outcome memo suppression later in the function, this can force the first pass to hit RECOGNITION_DEPTH_LIMIT and fail on otherwise valid inputs, triggering an expensive full retry every time for grammars with nullable recursion/cycles.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented May 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge — the new caching paths fall back to full ATN simulation on any cache miss or semantic-context state, the test suite passes at 357/357, and both benchmarks show the expected improvement.

The lexer DFA cache and parser lookahead helpers are well-contained and defensively implemented. All new code either falls back to existing ATN simulation on unexpected input or is gated behind the use_cache flag that is only set for the no-hooks default path. The only issues found are a redundant guard variable in match_token_cached and an Option return type that is never None — neither affects correctness.

No files require special attention. The lexer DFA caching in src/atn/lexer.rs is the most novel logic but is thoroughly guarded by fallback paths.

Fix All in Codex Fix All in Claude Code

Reviews (4): Last reviewed commit: "Avoid replay caching predicate-sensitive..." | Re-trigger Greptile

Comment thread src/parser.rs
Comment thread src/parser.rs
Comment thread src/atn/lexer.rs
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@greptile-apps Addressed the two latest parser follow-ups in c483bac.

  • Fast-pass single-outcome states are memoized again by caching any non-empty clean result, while still avoiding empty fast-pass cache entries.
  • FastRecognizedNode::MissingToken now reports a zero-width span as (at_index, None) instead of an inverted start > stop range.

Validated locally with cargo clippy --locked --all-targets --all-features -- -D warnings, cargo test --quiet, ParserExec/PredictionMode_SLL, LeftRecursion --limit 20, and ParserErrors/SingleSetInsertion.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

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

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🧹 Nitpick comments (2)
src/token_stream.rs (2)

15-15: 💤 Low value

Consider adding a doc comment for the sentinel constant.

A brief doc comment explaining why usize::MAX was chosen as the sentinel (e.g., "Sentinel value for uncached entries; valid token indices never reach usize::MAX") would improve code clarity.

🤖 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/token_stream.rs` at line 15, Add a short doc comment above the constant
UNKNOWN_NEXT_VISIBLE explaining that it is a sentinel for uncached/unknown
next-visible token indices and that usize::MAX is used because valid token
indices never reach that value (making it an unambiguous sentinel); update the
comment to mention the symbol name UNKNOWN_NEXT_VISIBLE and that it represents
"uncached entries" or "no next visible token" for clarity.

282-284: ⚡ Quick win

Add #[must_use] attribute to is_filled().

Pure getters should have the #[must_use] attribute to signal that ignoring the return value is likely a mistake. This aligns with Clippy's must_use_candidate pedantic lint. As per coding guidelines, Clippy pedantic lints should be treated as errors in CI.

📝 Proposed fix
+    #[must_use]
     pub const fn is_filled(&self) -> bool {
         self.fetched_eof
     }
🤖 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/token_stream.rs` around lines 282 - 284, The getter method is_filled()
should be annotated with #[must_use] so callers are warned if they ignore its
boolean result; add #[must_use] immediately above the pub const fn
is_filled(&self) -> bool definition (preserving pub/const) so Clippy's
must_use_candidate pedantic lint is satisfied.
🤖 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/atn/lexer.rs`:
- Around line 643-670: The cached DFA key currently built by
lexer_dfa_key(active, token_start) omits the predicate-sensitive flag
has_semantic_context, allowing predicate-crossing closures to collide; update
the cache identity so predicate-sensitivity is part of the key (e.g., include
has_semantic_context when computing lexer_dfa_key or add a new
lexer_dfa_key_with_semantic flag) and then call
lexer.cache_lexer_dfa_state(state, ...) as before, or alternatively skip caching
entirely when has_semantic_context is true; ensure references to lexer_dfa_key,
has_semantic_context, cache_lexer_dfa_state and LexerDfaCachedState are updated
consistently so predicate-sensitive states are not reused without predicate
re-evaluation.

---

Nitpick comments:
In `@src/token_stream.rs`:
- Line 15: Add a short doc comment above the constant UNKNOWN_NEXT_VISIBLE
explaining that it is a sentinel for uncached/unknown next-visible token indices
and that usize::MAX is used because valid token indices never reach that value
(making it an unambiguous sentinel); update the comment to mention the symbol
name UNKNOWN_NEXT_VISIBLE and that it represents "uncached entries" or "no next
visible token" for clarity.
- Around line 282-284: The getter method is_filled() should be annotated with
#[must_use] so callers are warned if they ignore its boolean result; add
#[must_use] immediately above the pub const fn is_filled(&self) -> bool
definition (preserving pub/const) so Clippy's must_use_candidate pedantic lint
is satisfied.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 982f45a5-712d-4d8e-ab3c-ffa2798bad75

📥 Commits

Reviewing files that changed from the base of the PR and between ea8f1e4 and c483bac.

📒 Files selected for processing (4)
  • src/atn/lexer.rs
  • src/lexer.rs
  • src/parser.rs
  • src/token_stream.rs

Comment thread src/atn/lexer.rs Outdated
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@greptile-apps Addressed the interner-invariant follow-up in 7a2f829.

FastRecognizeKey now documents directly that recovery_symbols must come from intern_recovery_symbols or empty_recovery_symbols, and calls out that bypassing the interner would split content-equal recovery sets into distinct cache coordinates.

Validated locally with cargo fmt --check and cargo test --quiet.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@greptile-apps

greptile-apps Bot commented May 23, 2026

Copy link
Copy Markdown

The documentation update in 7a2f829 looks good — the doc comment on FastRecognizeKey now makes the interner invariant explicit and explains the cache-correctness consequence of bypassing it (content-equal sets landing at different cache coordinates). That's exactly the right level of detail for a cache-key type: the invariant lives at the definition site, so any future caller constructing a key sees it immediately.

LGTM. Thanks for the clean follow-up.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@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: 7a2f82929d

ℹ️ 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/lexer.rs
Comment on lines +544 to +545
.entry(state)
.or_insert(cached_state);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve semantic-context flag when reusing DFA states

The cached lexer state is inserted with or_insert, so if a DFA state is first seen on a predicate-free path (has_semantic_context = false) and later reached through a path that does touch predicates, the stored flag never upgrades to true. match_token_cached uses this flag to decide whether transition replay is safe; a stale false allows replay across predicate-dependent edges and can skip required predicate re-evaluation, changing tokenization for grammars where predicate and non-predicate paths converge to the same config set.

Useful? React with 👍 / 👎.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ 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 merged commit f368e15 into main May 23, 2026
8 checks passed
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