Skip to content

Support dynamic lexer token emission - #96

Merged
tinovyatkin merged 2 commits into
mainfrom
codex/support-dynamic-lexer-token-emission
Jul 17, 2026
Merged

Support dynamic lexer token emission#96
tinovyatkin merged 2 commits into
mainfrom
codex/support-dynamic-lexer-token-emission

Conversation

@tinovyatkin

@tinovyatkin tinovyatkin commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • move the in-progress token type and channel onto BaseLexer so portable lexer commands and custom actions mutate the same emission state
  • expose action-context lookahead, consumption, type/channel overrides, accept-position control, and token-start advancement through LexerSemCtx
  • add a pending-token FIFO so one lexer match can produce a prefix token followed by its automatic token while each next_token call still appends exactly one token
  • cover MySQL-shaped determineFunction behavior, .identifier splitting, and EOF rewinds through the compiled lexer DFA, and document the expanded hook surface

Root cause and impact

Lexer action results were previously held in a private local value after ATN acceptance. Custom action hooks could change mode state but could not change the emitted token type or channel, and the runtime had no queue for multiple tokens from one match. That made standard ANTLR patterns used by MySQL-class lexers impossible to implement faithfully.

The shared pending-emission state and FIFO bring custom actions onto the same path as type, channel, skip, and more commands, making dynamic function/identifier classification, whitespace-channel changes, and dot-prefix splitting expressible without grammar-specific runtime behavior. EOF state is refreshed after cursor-moving actions so rewound suffixes remain available to the next token call.

Fixes #94.

Validation

  • cargo test --locked --all-features (344 passed)
  • full ANTLR runtime testsuite (356 passed, 0 failed, 1 existing skip)
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • rustdoc with warnings denied
  • git diff --check

Benchmark

Compared the issue implementation at 8a4846a399 against origin/main at 12712d34e on the same machine with generated Rust parsers only:

python3 tools/parse-bench/run.py \
  --languages kotlin,csharp,java,trino \
  --runtimes rust-antlr \
  --iters 10 --warmups 2 \
  --rust-generated-only

compare.py --max-regression 1.15 --runtime rust-antlr passed all 17 fixtures. Aggregate average time changed from 609.112 ms to 605.211 ms (-0.64%). The largest average increase was +2.79% on the 0.174 ms Trino Q21 fixture; larger fixtures remained within approximately +/-1.7%.

After the EOF-rewind review fix, the final head at adc3f4075 was compared against 8a4846a399 with the same 10-iteration setup. All 17 fixtures passed again; aggregate average time changed from 601.911 ms to 594.604 ms (-1.21%), with the largest increase +2.97% on the 0.170 ms Trino Q21 fixture. No regression was observed.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Summary
Lexer semantic hooks can now modify pending token type and channel, inspect lookahead, adjust token positions, and queue prefix tokens. BaseLexer tracks this state with FIFO emission and centralized EOF handling. ATN lexing applies actions directly to the shared lexer and flushes queued tokens before normal or EOF emission. Tests cover dynamic type/channel overrides, queued prefix tokens, and EOF rewinding. Documentation describes the expanded committed action context and speculative predicate restrictions.

Poem

I’m a rabbit with tokens tucked under my ear,
Queuing small prefixes, then making them clear.
Types hop and channels follow the trail,
Lookahead whispers softly through the rail.
One token per call, neat as can be—
The lexer now dances happily! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR appears to implement #94 by adding dynamic token-type overrides and hook-based emission behavior.
Out of Scope Changes check ✅ Passed The changes stay within runtime, docs, and tests for dynamic lexer emission, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly matches the main change: adding dynamic lexer token emission support.
Description check ✅ Passed The description is directly aligned with the lexer emission and semantic-hook changes in the pull request.

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 17, 2026

Copy link
Copy Markdown

Copy/Paste Detection

Found 18 duplication(s) across 3 changed Rust file(s) (threshold: 100 tokens).

Show duplications

Found a 38 line (198 tokens) duplication in the following files:

  • Starting at line 434 of src/atn/lexer.rs
  • Starting at line 498 of src/atn/lexer.rs
        atn,
        |lexer, action| {
            if !generated_action(lexer, action)
                && !dispatch_lexer_action_hook(&hooks, lexer, action)
                && unknown_policy == UnknownSemanticPolicy::Error
                && let (Ok(rule), Ok(index)) = (
                    usize::try_from(action.rule_index()),
                    usize::try_from(action.action_index()),
                )
            {
                lexer.record_semantic_error(true, rule, index);
            }
        },
        |lexer, predicate| {
            generated_predicate(lexer, predicate)
                .or_else(|| dispatch_lexer_predicate_hook(&hooks, lexer, predicate))
                .unwrap_or_else(|| match unknown_policy {
                    UnknownSemanticPolicy::AssumeTrue => true,
                    UnknownSemanticPolicy::AssumeFalse => false,
                    UnknownSemanticPolicy::Error => {
                        lexer.record_semantic_error(
                            false,
                            predicate.rule_index(),
                            predicate.pred_index(),
                        );
                        false
                    }
                })
        },
        accept_adjuster,
    );
    let token = token?;
    hooks.borrow_mut().lexer_token_emitted(
        sink.view(token)
            .expect("lexer hook token should be present in its sink"),
    );
    Ok(token)
}

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

  • Starting at line 12462 of src/parser.rs
  • Starting at line 12595 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 24 line (135 tokens) duplication in the following files:

  • Starting at line 195 of src/atn/lexer.rs
  • Starting at line 537 of src/atn/lexer.rs
pub fn next_token_with_hooks<I, A, P, E>(
    lexer: &mut BaseLexer<I>,
    sink: &mut TokenSink<'_>,
    atn: &LexerAtn,
    mut custom_action: A,
    mut semantic_predicate: P,
    mut accept_adjuster: E,
) -> Result<TokenId, TokenStoreError>
where
    I: CharStream,
    A: FnMut(&mut BaseLexer<I>, LexerCustomAction),
    P: FnMut(&BaseLexer<I>, LexerPredicate) -> bool,
    E: FnMut(&mut BaseLexer<I>, i32, usize),
{
    next_token_with_hooks_impl(
        lexer,
        sink,
        atn,
        &mut custom_action,
        &mut semantic_predicate,
        &mut accept_adjuster,
        LexerMatchStrategy {
            compiled: None,
            use_cache: false,

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

  • Starting at line 788 of src/atn/lexer.rs
  • Starting at line 905 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(atn, next, &mut |predicate| {
            semantic_predicate(lexer, predicate)
        });
        let target_has_semantic_context = closure.has_semantic_context;

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

  • Starting at line 11557 of src/parser.rs
  • Starting at line 11629 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 22 line (125 tokens) duplication in the following files:

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

  • Starting at line 7135 of src/parser.rs
  • Starting at line 7167 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 8025 of src/parser.rs
  • Starting at line 8098 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 12520 of src/parser.rs
  • Starting at line 12804 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 11994 of src/parser.rs
  • Starting at line 12120 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 10782 of src/parser.rs
  • Starting at line 11994 of src/parser.rs
  • Starting at line 12120 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 12253 of src/parser.rs
  • Starting at line 14109 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 12253 of src/parser.rs
  • Starting at line 14134 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 6351 of src/parser.rs
  • Starting at line 6721 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 14364 of src/parser.rs
  • Starting at line 14388 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 (106 tokens) duplication in the following files:

  • Starting at line 416 of src/atn/lexer.rs
  • Starting at line 479 of src/atn/lexer.rs
    atn: &LexerAtn,
    hooks: &mut H,
    mut generated_action: A,
    mut generated_predicate: P,
    unknown_policy: UnknownSemanticPolicy,
    accept_adjuster: E,
) -> Result<TokenId, TokenStoreError>
where
    I: CharStream,
    H: SemanticHooks,
    A: FnMut(&mut BaseLexer<I>, LexerCustomAction) -> bool,
    P: FnMut(&BaseLexer<I>, LexerPredicate) -> Option<bool>,
    E: FnMut(&mut BaseLexer<I>, i32, usize),
{
    let hooks = RefCell::new(hooks);
    let token = next_token_with_hooks(

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

  • Starting at line 5595 of src/parser.rs
  • Starting at line 6118 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 5340 of src/parser.rs
  • Starting at line 5364 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;
            };

@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 enhances the lexer and semantic context (LexerSemCtx) to support advanced token manipulation on the committed action path. Specifically, it allows custom lexer actions to override the pending token type and channel, consume characters, reset the accept position, advance the token start, and queue additional prefix tokens, enabling a single lexer match to emit multiple tokens. It also replaces the temporary LexerActionResult with direct mutations on BaseLexer and adds corresponding unit tests. There are no review comments to address, 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.

@tinovyatkin tinovyatkin changed the title [codex] Support dynamic lexer token emission Support dynamic lexer token emission Jul 17, 2026
@tinovyatkin
tinovyatkin marked this pull request as ready for review July 17, 2026 08:24
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 8a4846a399

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

@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/atn/lexer.rs`:
- Around line 687-695: Recompute the EOF state after accept_adjuster and any
cursor-repositioning actions, using the final lexer input position rather than
the earlier hit_eof value. Update the emit path around emit_position and
emit_or_enqueue_with_stop so rewinding from EOF allows the remaining suffix to
be processed, and add a regression test covering a rewind at EOF.
🪄 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: 53e0ddeb-6e56-42f7-9f72-ec01a8210ab7

📥 Commits

Reviewing files that changed from the base of the PR and between 12712d3 and 8a4846a.

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

Comment thread src/atn/lexer.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. Bravo.

Reviewed commit: adc3f40750

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

@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/atn/lexer.rs (1)

1663-1663: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the lookahead condition.

A direct boolean comparison is more idiomatic and readable here than a matches! macro with a pattern guard.

♻️ Proposed refactor
-            while matches!(ctx.la(1), value if value == ' ' as i32 || value == '\t' as i32) {
+            while ctx.la(1) == ' ' as i32 || ctx.la(1) == '\t' as i32 {
🤖 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/atn/lexer.rs` at line 1663, In the lexer loop around ctx.la(1), replace
the matches! pattern-guard condition with a direct boolean comparison that
checks whether the lookahead is a space or tab, preserving the existing
whitespace behavior.
🤖 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/atn/lexer.rs`:
- Line 1663: In the lexer loop around ctx.la(1), replace the matches!
pattern-guard condition with a direct boolean comparison that checks whether the
lookahead is a space or tab, preserving the existing whitespace behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbab4035-bdbc-4bb8-b81c-1c9dcd7e2dbb

📥 Commits

Reviewing files that changed from the base of the PR and between 8a4846a and adc3f40.

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

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@coderabbitai Regarding the outside-diff suggestion on src/atn/lexer.rs:1663: leaving this unchanged. The current matches! condition evaluates ctx.la(1) once per loop iteration; the proposed ctx.la(1) == space || ctx.la(1) == tab form evaluates lookahead a second time on the tab path. Since this regression test intentionally exercises the lookahead/consume hook surface, the single-evaluation form is both precise and avoids a redundant API call. There is no behavioral issue to address here.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@tinovyatkin You’re right. matches!(ctx.la(1), space | tab) performs one lookahead per iteration, while the disjunction may perform two. Keeping the existing condition is appropriate for this regression test; I’m withdrawing the suggestion.

🐇💭

@tinovyatkin
tinovyatkin merged commit 8532fc3 into main Jul 17, 2026
11 checks passed
@tinovyatkin
tinovyatkin deleted the codex/support-dynamic-lexer-token-emission branch July 17, 2026 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant