Skip to content

Implement semantic predicate accountability hooks#55

Open
tinovyatkin wants to merge 33 commits into
mainfrom
codex/issue-9-semantic-delivery
Open

Implement semantic predicate accountability hooks#55
tinovyatkin wants to merge 33 commits into
mainfrom
codex/issue-9-semantic-delivery

Conversation

@tinovyatkin

@tinovyatkin tinovyatkin commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • route parser semantic predicates and actions through a SemIR-backed ParserSemantics table while keeping the existing legacy predicate/action table API as a compatibility adapter
  • generate parser-owned semantic tables, direct SemIR predicate dispatch, fallback parser options, and typed hook adapters for helper-style semantics
  • add generator plan controls for --sem-patterns, --require-full-semantics, explicit --sem-unknown=hook, coordinate overrides, exact pattern/helper lowering, and JavaScript helper hook data in patterns/javascript.toml
  • expose the shared runtime pieces needed by the plan: SemIR constructors/evaluation trace support, parser hook execution context, lexer semantic hook wrappers, and LexerSemCtx
  • update README, CLAUDE, and the issue-9 design plan to document the delivered compatibility boundaries, fail-loud modes, hook strategy, and generator usage

Validation

  • cargo +1.95.0 check --locked --all-targets
  • cargo +1.95.0 clippy --locked --all-targets --all-features -- -D warnings
  • cargo +1.95.0 test --locked --all-targets
  • cargo +1.95.0 run --release --quiet --bin antlr4-runtime-testsuite -> summary: 357 passed, 0 failed, 0 skipped, 357 run
  • tests/kotlin-parity/run.sh --antlr-jar /tmp/antlr-cleanroom/tools/antlr-4.13.2-complete.jar --grammars-v4 /tmp/antlr-cleanroom/grammars-v4 --python /tmp/antlr-cleanroom/antlr-python/bin/python -> all 9 Kotlin/script snippets matched

Closes #9

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

tinovyatkin has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Copy/Paste Detection

Found 16 duplication(s) across 8 changed Rust file(s) (threshold: 100 tokens).

Show duplications

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

  • Starting at line 10294 of src/parser.rs
  • Starting at line 10379 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![CommonToken::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 23 line (133 tokens) duplication in the following files:

  • Starting at line 202 of src/atn/lexer.rs
  • Starting at line 392 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,
        LexerMatchStrategy {
            compiled: None,
            use_cache: false,

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

  • Starting at line 637 of src/atn/lexer.rs
  • Starting at line 755 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 19 line (120 tokens) duplication in the following files:

  • Starting at line 312 of src/bin_support/templates.rs
  • Starting at line 343 of src/bin_support/templates.rs
pub(crate) fn matching_action_brace(source: &str, mut index: usize) -> Option<usize> {
    let mut nested = 0_usize;
    let mut double_quoted = false;
    let mut escaped = false;
    while let Some(ch) = source[index..].chars().next() {
        if escaped {
            escaped = false;
            index += ch.len_utf8();
            continue;
        }
        match ch {
            '\\' if double_quoted => escaped = true,
            '"' => double_quoted = !double_quoted,
            '\'' if !double_quoted => {
                if let Some(next_index) = skip_char_literal(source, index) {
                    index = next_index;
                    continue;
                }
            }

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

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

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

  • Starting at line 6914 of src/parser.rs
  • Starting at line 6989 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 (114 tokens) duplication in the following files:

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

  • Starting at line 10345 of src/parser.rs
  • Starting at line 10582 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![
                    CommonToken::new(3).with_text("z"),
                    CommonToken::new(2).with_text("y"),

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

  • Starting at line 4777 of src/parser.rs
  • Starting at line 4820 of src/parser.rs
            FastRecognizedNode::Rule {
                rule_index,
                invoking_state,
                start_index,
                stop_index,
                children,
            } => {
                let mut context = ParserRuleContext::with_child_capacity(
                    *rule_index,
                    *invoking_state,
                    children.len(),
                );
                if let Some(token) = self.token_ref_at(*start_index) {
                    context.set_start_ref(token);
                }
                if let Some(token) = stop_index.and_then(|index| self.token_ref_at(index)) {
                    context.set_stop_ref(token);
                }
                if children.has_left_recursive_boundary() {
                    let folded = fold_fast_left_recursive_boundaries(children.to_vec());

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

  • Starting at line 11752 of src/parser.rs
  • Starting at line 11775 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 10108 of src/parser.rs
  • Starting at line 11664 of src/parser.rs
  • Starting at line 11689 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 20 line (107 tokens) duplication in the following files:

  • Starting at line 4510 of src/parser.rs
  • Starting at line 5046 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();
        let caller_follow_state = self.pending_invoking_follow_state(atn);

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

  • Starting at line 4762 of src/parser.rs
  • Starting at line 8416 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.as_str())
                    .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)))
            }

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

  • Starting at line 204 of src/atn/lexer.rs
  • Starting at line 343 of src/atn/lexer.rs
  • Starting at line 394 of src/atn/lexer.rs
    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,
        LexerMatchStrategy {
            compiled: None,

Found a 21 line (102 tokens) duplication in the following files:

  • Starting at line 5265 of src/parser.rs
  • Starting at line 5631 of src/parser.rs
    ) -> Option<RecognizeOutcome> {
        let (error_index, message) = self.expected_error_message(rule_index, start_index, expected);
        let token = self.token_at(error_index);
        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;
        }

Found a 12 line (102 tokens) duplication in the following files:

  • Starting at line 9730 of src/parser.rs
  • Starting at line 9770 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,

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • src/bin/antlr4-rust-gen.rs
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7586d186-a1de-4406-90bd-40389ce08b03

📥 Commits

Reviewing files that changed from the base of the PR and between 0bc64e8 and f72c6d9.

📒 Files selected for processing (1)
  • src/bin/antlr4-rust-gen.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR adds semantic-predicate/action documentation, a new semantic IR module, and hook-aware parser and lexer runtime support. It also updates code generation to inventory semantic coordinates, emit semantics.json, enforce --sem-unknown, and generate hook-capable parser types and constructors. Template scanning, recognizer metadata access, and JavaScript helper mappings were adjusted to support parser-side semantic hook handling.

Poem

I hopped through the grammar, a curious bun,
With hooks and SemIR all neatly spun.
The unknowns now speak, the manifests glow,
And predicates flutter wherever I go. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: semantic predicate/action hook support.
Description check ✅ Passed The description is clearly related to the implemented SemIR, hook, and documentation changes.
Linked Issues check ✅ Passed The PR matches #9 by adding fail-loud handling, hook-based escape hatches, stable mapping metadata, and boundary documentation.
Out of Scope Changes check ✅ Passed The changes stay centered on semantic predicate/action support and closely related runtime, generator, and docs updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

❤️ Share

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

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements the initial phases of the design for handling semantic predicates and actions (Issue #9). It introduces a new Semantic IR (semir module) to represent predicates and actions as data, adds support for user-defined SemanticHooks on the parser side, and implements a configurable policy (--sem-unknown) for handling unknown coordinates, complete with a semantics.json manifest output. The review feedback highlights two critical issues: a potential out-of-bounds panic in action_text when the start index exceeds the adjusted stop index, and a misalignment bug in parser_action_source_blocks that causes incorrect source spans and bodies to be recorded in the manifest for unsupported blocks.

Important

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

Comment thread src/parser.rs
Comment thread src/bin/antlr4-rust-gen.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: b8ccf2e3b3

ℹ️ 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 on lines 4483 to +4487
.collect::<Vec<_>>();
// A non-default policy must reach the interpreter through the emitted
// runtime options, so its literal forces the options-carrying call shape.
let unknown_policy_literal = match options.sem_unknown {
SemUnknownPolicy::AssumeTrue => None,

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 Disable adaptive direct for non-default unknown predicates

When --sem-unknown=assume-false or error is used on a parser whose ATN has only untranslated predicate transitions, unknown_policy_literal is set here but has_predicate_dispatch remains false, so the generated adaptive_direct_allowed gate can still take the ANTLR4_RUST_ADAPTIVE_DIRECT path. That helper falls back through parse_atn_rule(...) without the ParserRuntimeOptions emitted below, so the new unknown-predicate policy is lost and the predicate is treated as passing instead of failing/erroring. Please also disable the adaptive-direct path when a non-default unknown policy or unknown predicate coordinates are present.

Useful? React with 👍 / 👎.

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 3bcc2f2. adaptive_direct_allowed now also requires unknown_policy_literal.is_none(), so a non-default --sem-unknown policy disables the ANTLR4_RUST_ADAPTIVE_DIRECT shortcut. As you noted, that path runs parse_atn_rule_adaptive_or_fallback, which falls back through parse_atn_rule without the emitted ParserRuntimeOptions, dropping the policy. New test non_default_policy_disables_adaptive_direct_gate asserts the gate literal flips from && true && (default) to && false && under both assume-false and error, and first confirms the predicate-free fixture emits the enabled gate by default so the test can't silently pass.

@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`:
- Around line 424-444: In action_text(), the EOF fallback is using the wrong
boundary and can include hidden tokens before EOF. Update the stop calculation
to match text_interval() and $text by using the visible-token boundary for
TOKEN_EOF instead of blindly subtracting one, and keep the logic localized
around action.stop_index(), self.input.get(), and self.input.text() so the
returned action text excludes trailing whitespace.
🪄 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: bfd76bb4-9d5e-409c-b77e-b4357fa7ac13

📥 Commits

Reviewing files that changed from the base of the PR and between 879e22f and b8ccf2e.

📒 Files selected for processing (7)
  • CLAUDE.md
  • README.md
  • docs/issue-9-semantic-predicates-actions-design.md
  • src/bin/antlr4-rust-gen.rs
  • src/lib.rs
  • src/parser.rs
  • src/semir.rs

Comment thread src/parser.rs

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

tinovyatkin has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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: 568a824430

ℹ️ 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/bin/antlr4-rust-gen.rs

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

Caution

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

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

2382-2402: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid replaying legacy and SemIR actions for the same coordinate.

When semantics is present, these paths still apply legacy ParserMemberAction/ParserReturnAction tables first, then execute matching SemIR actions. If a generated parser passes both during migration, AddMember side effects can double-apply. Prefer SemIR for a matched action coordinate, falling back to legacy only when no SemIR action exists.

Also applies to: 2426-2444

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

In `@src/parser.rs` around lines 2382 - 2402, The replay logic in the parser state
handling is applying both legacy ParserMemberAction/ParserReturnAction entries
and SemIR actions for the same coordinate, which can double-apply member
updates. Update the code around the ParserTableSemCtx setup and the action
replay loop in parser::... so that when semantics is present you first check for
matching speculative SemIR actions and execute those instead, and only fall back
to the legacy actions when no SemIR action exists for that
source_state/coordinate. Make the same precedence change in the related block at
the other referenced location so both paths stay consistent.

4953-4954: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope and restore the active unknown-predicate policy.

This mutates parser-level unknown_predicate_policy and clears unknown_predicate_hits for one runtime-options parse, but never restores the previous state on success or error. A later direct predicate check can inherit Error/AssumeFalse from an earlier parse. Save the previous policy/hits and restore them before every return from this parse entry.

Also applies to: 5013-5016

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

In `@src/parser.rs` around lines 4953 - 4954, The runtime-options parse path is
leaving parser state behind by mutating Parser::unknown_predicate_policy and
clearing unknown_predicate_hits without restoring the prior values, so a later
predicate check can inherit the wrong policy. In the parse entry that touches
these fields, save the current unknown_predicate_policy and
unknown_predicate_hits before changing them, and restore both on every exit path
from the parse flow, including success and error returns.
🤖 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 `@docs/issue-9-semantic-predicates-actions-design.md`:
- Line 456: The markdownlint MD022 warning is caused by missing blank lines
around the Phase headings in the issue document. Update the markdown around the
`### Phase 3`, `### Phase 4`, and `### Phase 5` headings to insert the required
blank line separation before each heading, keeping the surrounding section text
unchanged. Use the heading markers themselves to locate the affected spots and
apply the same spacing fix consistently across those three sections.

In `@src/atn/lexer.rs`:
- Around line 229-274: The two semantic-hook entry points duplicate the same
action/predicate closure setup, so extract that wiring into a private helper.
Add a helper such as semantic_hook_closures that takes &RefCell<&mut H> and
returns the two closures used by next_token_with_semantic_hooks and
next_token_compiled_with_semantic_hooks, then have both functions create the
RefCell once and reuse the helper instead of inlining identical logic. Keep the
existing behavior in the closures (rule/action index conversion, LexerSemCtx
construction, and unwrap_or(true) default) unchanged.

In `@src/parser.rs`:
- Around line 2797-2811: The SemIR hook handling in `Parser::hook` is swallowing
missing `SemanticHooks::sempred` results by converting `None` into `false`,
which causes unregistered generated predicates to fail silently. Update
`Parser::hook` and the `SemanticHooks::sempred` call path to preserve the `None`
state for missing hooks and route it through the existing
unknown-coordinate/unsupported-hook handling instead of defaulting to rejection.

---

Outside diff comments:
In `@src/parser.rs`:
- Around line 2382-2402: The replay logic in the parser state handling is
applying both legacy ParserMemberAction/ParserReturnAction entries and SemIR
actions for the same coordinate, which can double-apply member updates. Update
the code around the ParserTableSemCtx setup and the action replay loop in
parser::... so that when semantics is present you first check for matching
speculative SemIR actions and execute those instead, and only fall back to the
legacy actions when no SemIR action exists for that source_state/coordinate.
Make the same precedence change in the related block at the other referenced
location so both paths stay consistent.
- Around line 4953-4954: The runtime-options parse path is leaving parser state
behind by mutating Parser::unknown_predicate_policy and clearing
unknown_predicate_hits without restoring the prior values, so a later predicate
check can inherit the wrong policy. In the parse entry that touches these
fields, save the current unknown_predicate_policy and unknown_predicate_hits
before changing them, and restore both on every exit path from the parse flow,
including success and error returns.
🪄 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: 100bf698-0678-4634-b1bc-c3e25ca0c070

📥 Commits

Reviewing files that changed from the base of the PR and between b8ccf2e and 568a824.

📒 Files selected for processing (10)
  • CLAUDE.md
  • README.md
  • docs/issue-9-semantic-predicates-actions-design.md
  • patterns/javascript.toml
  • src/atn/lexer.rs
  • src/bin/antlr4-rust-gen.rs
  • src/lexer.rs
  • src/lib.rs
  • src/parser.rs
  • src/semir.rs

Comment thread docs/issue-9-semantic-predicates-actions-design.md
Comment thread src/atn/lexer.rs
Comment thread src/parser.rs Outdated
Review pass over the SemIR delivery branch; validated with the full
conformance sweep (357/357), kotlin parity (9/9 trees match), unit
tests, and clippy -D warnings.

- Block walkers in bin_support/templates.rs now locate opening braces
  through a shared GrammarSourceCursor that skips quoted literals,
  comments, and charsets. Real grammars referencing brace tokens
  ('{' statementList? '}') previously desynchronized every predicate
  span/hook pairing: on grammars-v4 JavaScriptParser the manifest had
  0/16 predicate spans and 0 hook matches; now 16/16 with 11 routed to
  the typed hook trait.
- Hook-routed lexer predicates fail codegen with a clear error instead
  of panicking in the render path (generated lexers have no hook
  plumbing yet).
- Helper-hooked coordinates report disposition "hooked" instead of
  "translated" in semantics.json so users can tell which coordinates
  still need a runtime hook implementation.
- Predicate IR evaluation no longer clones the speculative member map
  and rule-name String on every evaluation inside the prediction loop;
  the predicate context is now fully borrowed and read-only.
- patterns/javascript.toml aligned with grammars-v4 JavaScriptParser:
  adds lineTerminatorAhead, drops the lexer-side isRegexPossible, and
  documents the argument-taking n()/p() helper gap.
- Design doc Phase 4/5 statuses tempered to partially-implemented with
  the concrete remaining gaps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

tinovyatkin has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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

Caution

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

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

4300-4324: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Surface unhandled parser actions instead of returning an ignored bool.

parser_action_hook returns whether the hook handled the committed action, but generated fallback code ignores that result. With default/no-op hooks, untranslated grammar actions can be skipped while parsing succeeds, which violates the fail-loud semantic-action boundary. Consider returning/propagating Result<(), AntlrError> or recording unsupported action hits like unknown predicates.

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

In `@src/parser.rs` around lines 4300 - 4324, The parser_action_hook path
currently returns a bool that can be silently ignored by generated fallback
code, allowing unsupported grammar actions to pass without surfacing an error.
Update parser_action_hook and its callers to propagate a failure signal instead
of treating the return value as optional, ideally using a Result-based flow or
equivalent error recording consistent with unknown predicate handling; reference
parser_action_hook, ParserSemCtx, and semantic_hooks.action when wiring this
through.
🤖 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 `@docs/issue-9-semantic-predicates-actions-design.md`:
- Line 482: The Phase 5 heading is missing the required blank line after it, so
update the markdown around the heading to ensure it is properly separated per
MD022. Locate the Phase 5 section in the document and add the missing empty line
immediately after the heading so the surrounding paragraph structure is correct.

---

Outside diff comments:
In `@src/parser.rs`:
- Around line 4300-4324: The parser_action_hook path currently returns a bool
that can be silently ignored by generated fallback code, allowing unsupported
grammar actions to pass without surfacing an error. Update parser_action_hook
and its callers to propagate a failure signal instead of treating the return
value as optional, ideally using a Result-based flow or equivalent error
recording consistent with unknown predicate handling; reference
parser_action_hook, ParserSemCtx, and semantic_hooks.action when wiring this
through.
🪄 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: 49417a6b-cbe4-4e51-9b30-b7492e852d9f

📥 Commits

Reviewing files that changed from the base of the PR and between 568a824 and c695193.

📒 Files selected for processing (6)
  • docs/issue-9-semantic-predicates-actions-design.md
  • patterns/javascript.toml
  • src/bin/antlr4-rust-gen.rs
  • src/bin_support/templates.rs
  • src/parser.rs
  • src/recognizer.rs

Comment thread docs/issue-9-semantic-predicates-actions-design.md

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

ℹ️ 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/bin/antlr4-rust-gen.rs
Comment thread src/parser.rs Outdated
Address PR #55 review findings, all in service of design goal G1
(never silently mis-parse):

- Parser SemIR hook: route a declining user hook (`None`) through the
  configured `UnknownSemanticPolicy` instead of `unwrap_or(false)`, so a
  `PExpr::Hook` coordinate with no implementation no longer silently
  rejects its alternative. Extracts a shared `apply_unknown_predicate_policy`
  so the SemIR and legacy table paths dispatch identically
  (hook -> policy). (Codex + CodeRabbit)
- Generator: disable the adaptive-direct shortcut whenever a non-default
  unknown-predicate policy is emitted; that path falls back through
  `parse_atn_rule` without the `ParserRuntimeOptions` carrying the policy,
  which would drop it. (Codex)
- Generator: the lexer `run_predicate` catch-all arm now follows
  `--sem-unknown` (`assume-false` -> `_ => false`), so a mixed lexer's
  uncovered predicate is not left viable. (Codex)
- `ParserSemCtx::action_text`: end the EOF interval at the previous
  *visible* token (matching `text_interval`/`$text`) instead of a blind
  `stop - 1`, excluding trailing hidden tokens. (Gemini + CodeRabbit)
- Manifest: pair each action-block source span with its ATN state through
  the same offset used for templates, walking signature templates in
  lockstep so span/body provenance no longer drifts after a
  `returns [<...>]` template. (Gemini)
- Deduplicate the two identical lexer semantic-hook closure bodies into
  `dispatch_lexer_action_hook` / `dispatch_lexer_predicate_hook`.
  (CodeRabbit + CPD)
- Docs: blank lines around Phase headings (markdownlint MD022).
  (CodeRabbit)

New regression tests cover the SemIR-hook policy fallthrough across all
three policies, the adaptive-direct gate flip, the lexer default-arm flip,
and action-slot/span alignment.

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

tinovyatkin has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Pushed 3bcc2f2 addressing the open review threads. Summary:

Fixed (fail-loud semantic boundary, G1):

  • Parser SemIR hook None now routes through UnknownSemanticPolicy instead of unwrap_or(false) — shared apply_unknown_predicate_policy unifies the SemIR and legacy paths (Codex + CodeRabbit, parser.rs).
  • Adaptive-direct shortcut disabled under any non-default --sem-unknown policy, which would otherwise drop the emitted ParserRuntimeOptions (Codex, gen.rs).
  • Lexer run_predicate catch-all arm now follows --sem-unknown (assume-false → _ => false) so a mixed lexer's uncovered predicate isn't left viable (Codex, gen.rs).

Fixed (other):

  • action_text EOF boundary uses the previous visible token, matching text_interval/$text (Gemini + CodeRabbit).
  • Manifest action-block span/body provenance no longer drifts after a returns [<...>] signature template — the span walk now mirrors the template walk and both share one state-assignment offset (Gemini).
  • Deduplicated the two lexer semantic-hook closures (CodeRabbit + CPD).
  • Markdown MD022 blank lines around Phase headings (CodeRabbit).

Two "outside diff range" notes, addressed as no-change-needed:

  • Double-apply of legacy + SemIR actions for one coordinate: generated parsers never pass both. Whenever semantics is emitted, the same call site passes member_actions: &[] / return_actions: &[] (see render_parser_parse_rule_fallback) and lowers those actions into parser_semantics() instead, so the legacy tables are always empty under SemIR. The legacy tables are documented deprecated adapters for pre-SemIR generated crates; a hand-written caller populating both simultaneously is out of scope.
  • parser_action_hook returns an ignored bool: this is the committed-action path; the fail-loud boundary for parser actions is enforced at codegen (untranslated actions become an --sem-unknown disposition in the manifest, and --require-full-semantics fails the build on policy fallbacks), mirroring how lexer actions already fail at generation time. Surfacing a runtime Result from the committed action path is a larger design change tracked with the remaining Phase 4/5 work rather than this review pass.

Validation: cargo clippy --locked --all-targets --all-features -- -D warnings clean; cargo test --locked green (added regression tests for all four code fixes); conformance SemPredEvalParser 26/26, SemPredEvalLexer 8/8, ParserExec green.

@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: 3bcc2f2a1b

ℹ️ 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/bin/antlr4-rust-gen.rs
Comment thread src/bin/antlr4-rust-gen.rs
Address the two P1 findings from Codex's review of 3bcc2f2, both deeper
instances of the fail-loud boundary (G1) in paths the first pass missed:

- Generated recursive-descent predicate evaluation reads
  `BaseParser::unknown_predicate_policy`, but only the interpreter fallback
  installed it (via `ParserRuntimeOptions`); the generated-direct path left
  it at the `AssumeTrue` default, so a hook predicate returning `None` in a
  generated rule silently passed even under `--sem-unknown=error`. Add a
  public `set_unknown_predicate_policy` setter and have the generated parser
  constructor install a non-default policy, so both paths honor
  `--sem-unknown`. Also add `take_unknown_semantic_error` so the generated
  path can surface recorded `Error`-policy coordinates.

- Under `--sem-unknown=hook`/`error`, an *uncovered* lexer predicate was
  marked `hooked` in the manifest and accepted by `--require-full-semantics`,
  yet generated lexers have no hook plumbing (a `hook`-lowered lexer
  predicate is already a codegen error) and no runtime coordinate recording,
  so `run_predicate`'s catch-all silently kept it viable. `render_lexer` now
  rejects uncovered lexer predicates under hook/error, mirroring the existing
  explicit-hook rejection.

Tests: generated constructor installs the policy literal (codegen) and the
generated-direct matcher honors it end-to-end (runtime, all three policies);
uncovered lexer predicates are rejected under hook and error.

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

tinovyatkin has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Pushed 88e9fbd addressing both P1 findings from the last review — both were deeper instances of the fail-loud boundary (G1) in paths the previous commit didn't reach:

  • Generated-direct predicate policy: the generated recursive-descent path read BaseParser::unknown_predicate_policy but only the interpreter fallback installed it, so it stayed at the AssumeTrue default. Added set_unknown_predicate_policy + a generated-constructor install, so a declined hook predicate in a generated rule now honors --sem-unknown (fatal failed_predicate_error under error/assume-false; take_unknown_semantic_error surfaces recorded coordinates).
  • Uncovered lexer predicate under hook/error: these were dispositioned hooked and accepted by --require-full-semantics, but generated lexers have no hook plumbing and no runtime recording. render_lexer now rejects them, mirroring the existing explicit-hook rejection.

Validation: cargo clippy --locked --all-targets --all-features -- -D warnings clean; cargo test --locked green (added 3 regression tests — generated-constructor policy install, generated-direct runtime honoring across all three policies, lexer hook/error rejection); conformance re-run SemPredEvalParser 26/26 and SemPredEvalLexer 8/8.

@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


P1 Badge Surface generated unknown semantic hits before success

When a generated semantic decision evaluates a hook-lowered predicate and the hook returns None, parser_semantic_ir_predicate_matches_with_context_and_local records the coordinate under the Error fallback and returns false; the interpreter path checks this after recognition, but this generated success path returns the tree without draining take_unknown_semantic_error(). In that scenario the unknown predicate can merely prune an alternative and still produce a parse tree instead of the promised AntlrError::Unsupported, so add the same recorded-error check before returning success.


"{pad} return Err(self.base.failed_predicate_error(\"semantic predicate\"));"

P1 Badge Return the recorded unknown semantic error

For a hook-lowered predicate reached as an actual generated rule step, an unhandled hook under the Error fallback records the unsupported coordinate and returns false, but this branch converts it into a generic failed-predicate ParserError. That loses the coordinate and violates the fail-loud Unsupported behavior; check take_unknown_semantic_error() before falling back to failed_predicate_error here.

ℹ️ 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/bin/antlr4-rust-gen.rs
Address Codex P2 (review of 88e9fbd): the predicate-block scans, unlike the
action collectors, never applied rule-name filtering. In a combined grammar
(`--grammar` = one .g4 with both lexer and parser rules) this walked the other
rule set's `{...}?` predicates into the positional pairing against this ATN's
predicate transitions — mis-mapping a translatable predicate onto the wrong
coordinate, or erroring with "no parser ATN predicate transition".

`parser_predicate_templates` and the new `extract_supported_predicate_templates_filtered`
now gate each predicate block through `rule_action_included` (the same filter
the action collectors use), skipping blocks that belong to a different rule set
so coordinate pairing stays aligned. `lexer_predicate_templates` passes the
lexer's rule names; the parser scan passes the parser's.

Tests: a translatable lexer-rule predicate preceding a parser rule is skipped
(not mapped onto the parser coordinate), and the parser-rule predicate still
maps to its correct coordinate.

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

tinovyatkin has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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

if predicates.len() != templates.len() {
return Err(io::Error::new(

P2 Badge Let mixed lexer predicates use policy fallbacks

For a lexer with one translated predicate and one untranslated predicate under --sem-unknown=assume-false or assume-true, this equality check errors as soon as the template count differs, so the uncovered-coordinate logic and the generated catch-all arm never run. Fresh evidence is that render_lexer_predicate_method now has a policy-aware default arm for mixed lexers, but lexer_predicate_templates still rejects those mixed inputs before rendering.

ℹ️ 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
Comment thread src/bin/antlr4-rust-gen.rs
Comment thread src/bin/antlr4-rust-gen.rs
The predicate rule-name filter added in bfee243 regressed
SemPredEvalParser/ValidateInDFA: its grammar has `// ';' helps ...` above
rule `a`, and the brace-counting `statement_rule_header` matches that `;`
inside the comment as a rule terminator, so header resolution fails
(`None`). `rule_action_included` treats an unresolvable header as "not in the
filter set" and skipped rule `a`'s `{<False()>}?`/`{<True()>}?` predicates,
dropping them from `parser_semantics()` — the guarded alternatives then went
unguarded (`alt 1` printed instead of no-viable-alt).

Introduce `predicate_block_included`, which excludes a predicate block only
when its owning rule name *positively resolves* and is absent from the target
rule set; an unresolvable header keeps the block (pre-filter behavior). Both
predicate scans use it, so the combined-grammar lexer/parser separation still
holds without dropping real predicates when the scraper is defeated by
comments.

Test: `parser_predicate_scan_keeps_predicate_when_header_unresolvable` pins
the comment-with-semicolon case; SemPredEvalParser is back to 26/26.

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

tinovyatkin has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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

if predicates.len() != templates.len() {

P2 Badge Allow fallback for partially translated lexer predicates

When a lexer has a mix of one predicate that matches a built-in/pattern template and another raw predicate that should follow --sem-unknown=assume-true or assume-false, extract_supported_predicate_templates_filtered returns only the translated entries, so this count check rejects generation before the fallback arm rendered in run_predicate can apply. The all-unknown case is accepted via templates.is_empty(), so adding a single translatable predicate makes the same uncovered coordinate a hard codegen error instead of using the documented fallback policy.


| ActionTemplate::SetMember { .. }

P2 Badge Preserve SetMember effects in interpreted rules

For parser rules that fall back to the ATN interpreter, supported member mutations are replayed speculatively through the SemIR action table, but this arm drops SetMember while only AddMember is collected. A rule with SetMember("i", "3") followed by a MemberEquals("i", "3") predicate will work on the generated-direct path (which inlines set_int_member) but the interpreted fallback leaves i at its previous value and can choose/reject the wrong alternative.

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

Address Codex's three findings on bfee243:

- Generated top-level rule entry now calls `take_unknown_semantic_error`
  before returning `Ok`, so Error-policy coordinates the generated-direct
  predicate path recorded surface as `AntlrError::Unsupported` instead of a
  recovered `Ok` tree. Wires up the accessor added in 88e9fbd (previously
  only reachable from tests). Surfaced at the public entry
  (`allow_generated_fallback`) so nested rules accumulate and the outermost
  reports, mirroring the interpreter entry. (P1)

- `parser_typed_hook_mappings` now applies the same `predicate_block_included`
  rule-name filter as `parser_predicate_templates`, so a combined grammar's
  lexer-rule helper predicate no longer consumes a parser coordinate and wires
  `MyParserTypedHooks` to the wrong method. (P2)

- `enforce_sem_unknown` now fails codegen for any per-coordinate
  `dispose = "error"` override regardless of the global policy. Such an
  override lowers to no SemIR entry and does not escalate the runtime policy,
  so previously it silently fell back to the global default (e.g. AssumeTrue)
  instead of rejecting the coordinate. (P2)

Tests: generated entry emits the surfacing call; typed-hook mapping skips a
lexer-rule predicate preceding the parser helper; a per-coordinate error
override fails even under assume-true.
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Keep assume-action overrides out of hook fallback (gen.rs:8281)

Fixed in 053d7e1. Good follow-up — this was a gap my previous assume-* fix opened.

Dropping the translated run_action arm for an assume-true / assume-false action made it fall through to the _ => parser_action_hook(...) catch-all. Under --sem-unknown=hook (which installs UnknownSemanticPolicy::Error), that meant NoSemanticHooks recorded an unhandled action and failed the parse — and a custom hook could run a side effect — for a coordinate the manifest reports as a silent no-op fallback.

The two disposition classes now get different fallbacks: assume-* action states get an explicit state => {} arm emitted before the hook catch-all, so they are a true no-op; only hook/error (and genuinely unknown) states reach parser_action_hook:

fn run_action(&mut self, action: antlr4_runtime::ParserAction, _tree: &antlr4_runtime::ParseTree) {
    match action.source_state() {
        3 => {}                                                        // assume-* override: silent no-op
        _ => { let _ = self.base.parser_action_hook(action, _tree); }  // hook / unknown
    }
}

The scan walks all ATN action states (parser_action_state_rules), not just translated ones, so an untranslatable action (never in the templated actions set) with an assume-* override is also covered — that was the subtle part, since such a state never appears in the translated-action list.

Verified end-to-end:

  • assume-true override + --sem-unknown=hook + NoSemanticHooks → parses Ok (was: fail-loud Unsupported).
  • hook override on the same coordinate → still routes to parser_action_hook (no no-op arm), so the escape hatch is unaffected.

Test parser_action_assume_override_gets_explicit_noop_arm pins the no-op arm ordering ahead of the hook catch-all. Full 357-descriptor conformance sweep green.

Addresses two Codex findings about `@init` member effects not being
visible to same-rule predicates/actions. Investigation showed both rest
on a deeper rule-name-resolution gap, so the fix is three connected parts:

1. Rule-name resolution (`named_action_rule_name`): a `@members` /
   `@parser::members` block between the previous `;` and a rule's `@init`
   no longer hides the rule name. The old code truncated the header at the
   first `@` after the last `;`, so the rule name (which follows the
   members block) was dropped and the rule's `@init` was silently
   discarded on every path. Now `last_rule_header_identifier` skips
   `@name {...}` blocks (tracking brace depth so a `{...}` inside a
   `<...>` template body doesn't confuse the skip) and returns the
   trailing rule identifier. Shared by `@init` and `@after`.

2. Mixed `@init` sequences (`init_action_member_writes`): a sequence like
   `SetMember(x,1); writeln(...)` now hoists its member writes to rule
   ENTRY (so same-rule predicates observe them) while the side effect
   stays on the buffered exit-replay path. Previously the all-or-nothing
   `init_action_mutates_members_only` gate excluded the whole sequence
   from entry execution.

3. Together these make the generated-direct `@init`-before-body order
   correct: verified end-to-end that a rule with
   `@parser::members {<InitIntMember("i","0")>}` and
   `s @init {<SetMember("i","1")>} : {<MemberEquals("i","1")>}? ...` now
   parses without a failed-predicate diagnostic (the predicate observes
   i==1), where it previously failed the predicate against the pre-init
   value.

The interpreter-fallback variant of the finding is not reachable in
practice: a rule carrying `@init` + a same-rule predicate compiles
generated-direct (confirmed with wildcard and left-recursive shapes), so
the eager entry-write on the generated path is what governs correctness.

Tests: `init_action_rule_name_resolves_past_a_members_block`,
`init_member_write_after_members_block_runs_at_entry`,
`init_entry_extracts_member_writes_from_mixed_sequence`. clippy clean;
full suite green.
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

| ActionTemplate::SetMember { .. }

P2 Badge Lower SetMember for interpreted semantic replay

When a rule falls back to the ATN interpreter, member-mutating actions are replayed speculatively through parser_member_actions/SemIR so later member predicates see earlier actions on the same path. This arm classifies SetMember as having no speculative effect, so an interpreted rule such as {<SetMember("i","1")>} {<MemberEquals("i","1")>}? evaluates the predicate against the old member value and can reject a path that ANTLR accepts; SetMember needs to be lowered to AStmt::SetMember just like AddMember is lowered to AddMember.


"{pad} if tree.is_none() && CTX_ROOTED_ACTION_STATES.contains(&action.source_state()) {{"

P2 Badge Preserve child context for action hooks

For a generated child rule whose action state is routed to parser_action_hook (for example by a per-coordinate dispose = "hook" override), the buffered action is only retagged with the child tree when the state is in CTX_ROOTED_ACTION_STATES. Hook-routed or otherwise unknown actions usually are not in that translated-template set, so replay calls run_action with the parent tree; parser_action_hook then cannot populate ParserSemCtx::context for the action's own rule. Hooked actions should be tagged with the child tree as well, otherwise nested action hooks observe the wrong/no $ctx context.


self.run_after_actions(rule_index, &__tree, start_index, stop_index);

P2 Badge Check action-hook failures before running @after

In the top-level interpreted path, parse_interpreted_rule_precedence replays parser actions before returning, and an unhandled hook-disposed action can record a fail-loud semantic error there. This @after call runs before the subsequent take_unknown_semantic_error() check, so a parse that is about to return AntlrError::Unsupported can still execute @after side effects; check the recorded semantic error before running top-level interpreted @after actions.

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

Re: Apply @init member effects before interpreted predicates (parser.rs:5120-5124) and Preserve member writes in mixed @init sequences (gen.rs:7890-7892)

Fixed in 36e5e83 (both findings). Investigating these surfaced a deeper root cause, so the fix is three connected parts.

Root cause (rule-name resolution). named_action_rule_name resolved a rule's name for @init/@after by taking the header text after the last ; and calling .split('@').next() — truncating at the first @. When a @members / @parser::members block sits between the last ; and the rule (the normal layout when a rule's @init writes a member the block declares), the rule name — which comes after the block — was discarded, so the rule's @init was silently dropped on every path (its member write never happened at all). This is why the effect wasn't observable before: the @init action wasn't being parsed onto any rule.

last_rule_header_identifier now skips @name {...} / @name::sub {...} blocks including their brace bodies (tracking brace depth so a {...} inside a <...> template body doesn't confuse the skip) and returns the trailing rule identifier.

Finding 2 (mixed @init sequences). The init_action_mutates_members_only gate was all-or-nothing: a sequence like SetMember(x,1); writeln(...) was excluded from entry execution entirely, so the member write only happened on exit-replay. init_action_member_writes now extracts just the SetMember/AddMember subactions to run at rule ENTRY (idempotent under the checkpoint/restore + replay cycle), while the full sequence stays on the buffered exit-replay path so the side effect isn't duplicated and its ordering matches ANTLR.

Finding 1 (interpreter fallback). With the two fixes above, the generated-direct path now applies @init member writes at entry (before the body), so a same-rule predicate observes them. I could not force the interpreter fallback for a rule carrying @init + a same-rule predicate — such rules compile generated-direct (confirmed with wildcard and left-recursive shapes) — so the eager entry-write on the generated path governs correctness here. If you have a grammar that does reach the interpreter with this shape, I'll wire the entry-writes through ParserRuntimeOptions as well; I left that out rather than add runtime plumbing I couldn't exercise with a real grammar.

Verified end-to-end. A grammar with @parser::members {<InitIntMember("i","0")>} and s @init {<SetMember("i","1")>} : {<MemberEquals("i","1")>}? ID EOF ; previously failed the predicate (recovered to a partial tree with a failed predicate diagnostic) because the predicate saw i==0; it now parses cleanly with the predicate observing i==1.

Tests: init_action_rule_name_resolves_past_a_members_block, init_member_write_after_members_block_runs_at_entry, init_entry_extracts_member_writes_from_mixed_sequence. named_action_rule_name is shared with @after, so the full 357-descriptor conformance sweep was re-run green.

When a semantic decision's predicted alt has its leading predicate fail,
`render_semantic_alt_search` picks the first *other* alt whose candidate
condition holds. An alt with no locally-checkable guard — no leading
predicate and no computable lookahead, because its first consuming step
is a rule call / nested decision / loop — has candidate condition `"true"`,
so it shadowed a later alt with a concrete matching lookahead. A grammar
like `{p()}? 'a' | x | 'a'` (with `x : 'b'`) on input `a` therefore
selected the non-viable rule-call alt `x` after `p` failed, instead of
the viable `'a'` alt.

The fallback search now forces such an unresolved-guard alt to `false`
(new `semantic_alt_guard_is_unresolved`), so it is skipped rather than
shadowing a concretely-guarded alt. A genuine epsilon alt (no consuming
step) stays matchable.

This is scoped to the fallback search only: the shared
`semantic_alt_candidate_condition` is unchanged, so left-recursion
loop-entry (`render_generated_loop_semantic_prediction_filter`) and the
diagnostic path keep their behavior — verified that
`DuplicatedLeftRecursiveCall` / `PrefixAndOtherAlt` codegen is byte
-identical to before. The ATN-predicted alt is still honored via the
`_ => Some(__prediction.alt)` arm, so a rule-call alt that is itself the
predicted alt is unaffected.

Verified end-to-end: `{p()}? 'a' | x | 'a'` on input `a` (p false) now
parses `Ok` by selecting the `'a'` alt.

Tests: `semantic_alt_guard_classifies_unresolved_rule_call_alt`,
`semantic_alt_search_skips_unresolved_rule_call_alt`. clippy clean; full
suite green.

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

ℹ️ 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/bin/antlr4-rust-gen.rs Outdated
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Guard rule-call alternatives in semantic alt search (gen.rs:4425-4428)

Fixed in dfe9cab. Correct — an alt with an uncomputable FIRST set was being treated as an unconditional candidate.

semantic_alt_candidate_condition collapsed to "true" for two different situations: a genuine epsilon alt (legitimately matches anything) and an alt whose first consuming step is a CallRule / nested decision / loop (its FIRST set isn't computed locally). In the ordered render_semantic_alt_search fallback, the second case won unconditionally, so {p()}? 'a' | x | 'a' (with x : 'b') on input a selected the rule-call alt x after p failed, instead of the later viable 'a' alt.

The fix distinguishes the two: a new leading_step_guard_is_uncomputable helper returns true when the first consuming step is a CallRule/decision/loop with no leading predicate or computable lookahead. Such an alt now renders as false in the candidate condition, so the fallback search skips it rather than letting it shadow a later alt with a concrete matching lookahead. A genuine epsilon alt (no consuming step) stays true.

This only changes the fallback search triggered when another alt's predicate fails: the ATN-predicted alt is still honored via the _ => Some(__prediction.alt) arm, so a rule-call alt that is itself the predicted alt is unaffected. The diagnostic path already bailed out on "true"/"false" conditions, so it stays correct too.

I initially tried this in the shared semantic_alt_candidate_condition and the conformance sweep caught that it broke left-recursion (DuplicatedLeftRecursiveCall, PrefixAndOtherAlt) — that helper is also used by the left-recursion loop-entry filter, where a Precedence-led alt legitimately has no lookahead. The committed fix is scoped to the fallback search only, and I confirmed the left-recursion codegen is byte-identical to before.

Tests: semantic_alt_guard_classifies_unresolved_rule_call_alt, semantic_alt_search_skips_unresolved_rule_call_alt. Full 357-descriptor conformance sweep green.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

All findings from your incremental reviews are now addressed through HEAD dfe9cab. Since your last batch:

  • assume-* action → hook fallback (053d7e1): assume-* action states get an explicit no-op arm instead of falling through to parser_action_hook.
  • @init member writes before same-rule predicates (36e5e83): fixed the underlying rule-name-resolution gap (a @members block between the last ; and a rule's @init no longer hides the rule name), plus mixed-@init-sequence member-write extraction. Verified end-to-end.
  • Guard rule-call alternatives in semantic alt search (dfe9cab): an unresolved-FIRST alt (rule-call/decision/loop, no predicate/lookahead) is now skipped in the fallback search instead of shadowing a later concretely-guarded alt. Scoped to the fallback search so left-recursion loop-entry is untouched (codegen byte-identical); verified with your exact {p()}? 'a' | x | 'a' example.

Validation on dfe9cab: all 7 CI checks green, including the full 357-descriptor Runtime Testsuite. clippy -D warnings clean; cargo test --locked green. Each finding has a per-comment reply with the details.

@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

match template {
PredicateTemplate::False => PredicateTemplate::FalseWithMessage { message },
_ => template,

P2 Badge Preserve fail-option messages for all predicates

When a grammar attaches <fail=...> to any translated predicate other than a constant false (for example a hook, lookahead, or member predicate that returns false at runtime), this match returns the original template and discards message. Later parser_semantic_ir_predicate_failure_message can only recover with ANTLR's fail text when the template became FalseWithMessage, so those predicates fall back to the generic failed/no-viable path despite the grammar supplying a fail option. Store the fail message alongside every predicate template, not only PredicateTemplate::False.

ℹ️ 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/bin/antlr4-rust-gen.rs Outdated
Follow-up to the semantic-alt fallback-search fix: forcing an
unresolved-FIRST alt to `if false` stopped it shadowing a later
concretely-guarded alt, but also made it unreachable when it was the only
remaining candidate — `{p()}? 'a' | x` with `p()` false and input in
`FIRST(x)` reported `NoViableAlt` instead of trying `x`.

`render_semantic_alt_search` now runs in two passes: resolved-guard alts
(predicate and/or lookahead) first, in order, then the unresolved alts in
order as a last resort (keeping their real condition, typically `true`).
So a resolved alt is never shadowed by an earlier unresolved rule-call
alt, and an unresolved alt is still selected when no resolved alt matched.

Verified end-to-end:
- `{p()}? 'a' | x` (x : 'b'), input `b`, p false -> selects `x` (was
  NoViableAlt).
- `{p()}? 'a' | x | 'a'`, input `a`, p false -> selects the `'a'` alt (no
  shadowing by rule-call `x`).

Still scoped to the fallback search; the shared
`semantic_alt_candidate_condition` and left-recursion loop-entry are
unchanged. Tests: `semantic_alt_search_orders_unresolved_alts_last`,
`semantic_alt_search_keeps_lone_unresolved_alt_reachable`. clippy clean;
full suite green.

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

ℹ️ 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/atn/lexer.rs Outdated
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Keep unresolved semantic alternatives as fallbacks (gen.rs:4359)

Fixed in e717e01. You're right — forcing the unresolved alt to if false fixed the shadowing but over-corrected: it made the alt unreachable when it was the only remaining candidate, so {p()}? 'a' | x with p() false and input in FIRST(x) reported NoViableAlt instead of trying x.

render_semantic_alt_search now runs in two passes:

  1. Resolved-guard alts (predicate and/or computable lookahead), in order. A later token-led alt is reachable even when an earlier unresolved rule-call alt exists — so no shadowing.
  2. Unresolved alts, in order, as a last resort, keeping their real condition (typically true). So an unresolved alt is still selected when no resolved alt matched.

For {p()}? 'a' | x | 'a' the emitted search (after alt 1's predicate fails) is now:

if __semantic_la == 1 && <pred> { Some(1) }      // pass 1: resolved
else if __semantic_la == 1 { Some(3) }           // pass 1: resolved (token alt, not shadowed)
else if true { Some(2) }                          // pass 2: unresolved rule-call, last resort
else { None }

Verified end-to-end:

  • {p()}? 'a' | x (x : 'b'), input b, p false → selects x (was NoViableAlt).
  • {p()}? 'a' | x | 'a', input a, p false → selects the 'a' alt (rule-call x does not shadow it).

Still scoped to the fallback search; the shared semantic_alt_candidate_condition and left-recursion loop-entry are untouched. Tests: semantic_alt_search_orders_unresolved_alts_last, semantic_alt_search_keeps_lone_unresolved_alt_reachable. Full 357-descriptor conformance sweep green (CI + local).

A grammar's `<fail='...'>` option was only preserved for a constant-false
predicate (folded into `FalseWithMessage`). For any other translated
predicate that can return false at runtime — a hook, lookahead, member,
or local-int predicate — `predicate_template_with_fail_message` discarded
the message, so `parser_semantic_ir_predicate_failure_message` had nothing
to surface and the parse fell back to the generic failed-predicate text
despite the grammar supplying a fail message.

Introduce a transparent `PredicateTemplate::WithFailMessage { inner,
message }` wrapper that carries the message alongside any template. It is
transparent to evaluation and codegen: `predicate_effective_template`
unwraps it wherever the inner template's behavior matters
(`render_lexer_predicate_expression`, `render_parser_semir_predicate_expr`,
`render_parser_predicate_array`, `can_generate_parser_predicate`,
`predicate_template_disposition`), and `predicate_template_fail_message`
exposes the message so the SemIR predicate builder emits `failure_message`
for every predicate that carries one, not only `FalseWithMessage`.

Verified end-to-end: `{<MemberEquals("i","1")>}?<fail='member not one'>`
with `i` defaulting to 0 now surfaces `rule s member not one` instead of
the generic failed-predicate diagnostic; the old generator emitted
`failure_message: None` for that coordinate.

Tests: `parses_predicate_fail_option_message` extended to cover the
wrapper (hook predicate), its transparency (effective template,
generatability, disposition = Hooked), the exposed message, and message
replacement on a second `<fail=...>`. clippy clean; full suite green.
`parse_sem_patterns` stripped everything after the first `#` on a line to
drop TOML comments, but a `#` inside a quoted string — e.g.
`match = "text == '#'"` or a `str("#")` lower expression — was treated as
a comment too, silently corrupting valid pattern files so the intended
pattern failed to match.

`strip_toml_comment` now scans for a comment `#` only outside quoted
strings (basic `"..."` and literal `'...'`), honoring `\"` escapes inside
a basic string so an escaped quote does not appear to close it.

Tests: `strip_toml_comment_respects_quoted_strings` (unit cases for
comment vs. in-string `#`, post-string comment, and escaped quote) and
`sem_patterns_keep_hash_inside_quoted_match` (end-to-end: a `#` inside a
quoted `match` body survives parsing and still lowers). clippy clean; full
suite green.

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

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

/// `true` when the action is handled so it satisfies a `hook`/`error`
/// unknown-semantic policy; the default no-op returns `false` (unhandled),
/// which fails loud under those policies.
fn custom_action<S>(&mut self, _ctx: &mut antlr4_runtime::ParserSemCtx<'_, S>, _action: antlr4_runtime::ParserAction) -> bool

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 Avoid colliding typed hooks with custom_action

When a grammar has a bare predicate helper whose Rust name normalizes to custom_action (for example customAction() or custom_action()), method_decls emits a required typed predicate method with that name and this default action hook method emits another custom_action in the same trait. Rust traits cannot overload methods by arity, so the generated parser fails to compile for an otherwise valid hook mapping; reserve/disambiguate this generated action-hook name.

Useful? React with 👍 / 👎.

`next_token_with_semantic_hooks` dispatched a custom lexer action through
`LexerSemCtx` built from `&BaseLexer`, so a `SemanticHooks::lexer_action`
hook could not change lexer state — a hook equivalent to
`pushMode()`/`popMode()`/`setMode()` had no effect even when it returned
handled, unlike the closure-based `custom_action` API which receives
`&mut BaseLexer`. The accepted token would then be followed by lexing in
the wrong mode.

`LexerSemCtx` now holds a `LexerRef` that is either a shared borrow (for
the speculative `lexer_sempred` path, where mutation is invalid) or a
mutable borrow (for the committed `lexer_action` path). The action
dispatch builds the mutable variant via `new_mut`, and the context exposes
`set_mode` / `push_mode` / `pop_mode` that apply on the action path and are
inert (returning `false`/`None`) on the predicate path. Read accessors work
on both.

Tests: `lexer_action_hook_context_can_change_mode` verifies mode
push/pop/set apply through the mutable context and are inert through the
shared one. clippy clean; full suite green.
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Preserve fail-option messages for all predicates (gen.rs:6214-6216)

Fixed in c8c92e0. Correct — the fail message was preserved only for a constant-false predicate (folded into FalseWithMessage); for any other translated predicate that can return false at runtime (hook, lookahead, member, local-int), predicate_template_with_fail_message discarded it, so parser_semantic_ir_predicate_failure_message had nothing to surface.

Introduced a transparent PredicateTemplate::WithFailMessage { inner, message } wrapper that carries the message alongside any template. It's transparent to evaluation and codegen — predicate_effective_template unwraps it wherever the inner behavior matters (render_lexer_predicate_expression, render_parser_semir_predicate_expr, render_parser_predicate_array, can_generate_parser_predicate, predicate_template_disposition), and predicate_template_fail_message exposes the message so the SemIR predicate builder emits failure_message for every predicate that carries one:

let failure_message = predicate_template_fail_message(predicate).map_or_else(
    || "None".to_owned(),
    |message| format!("Some(\"{}\")", rust_string(message)),
);

Verified end-to-end with {<MemberEquals("i","1")>}?<fail='member not one'> (i defaults to 0, so the predicate fails):

  • Generated code now emits failure_message: Some("member not one") for the coordinate (the pre-fix generator emitted failure_message: None).
  • The parser surfaces rule s member not one instead of the generic failed-predicate text.

I took care that the wrapper doesn't mis-classify: predicate_template_disposition and can_generate_parser_predicate unwrap first, so a <fail=...>-wrapped hook is still reported Hooked in the manifest and stays generatable. (The wrapper is only ever created in the parser predicate path, so lexer predicate handling is unaffected.)

Test: parses_predicate_fail_option_message extended to cover the wrapper — its transparency (effective template, generatability, Hooked disposition), the exposed message, and message replacement on a second <fail=...>.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Don't treat # inside pattern strings as comments (gen.rs:1068, P3)

Fixed in 19d1f85. Correct — parse_sem_patterns stripped everything after the first # on a line, even when the # was inside a quoted string (match = "text == '#'" or a str("#") lower), silently corrupting the pattern so it failed to match.

strip_toml_comment now scans for a comment # only outside quoted strings — basic "..." and literal '...' — and honors \" escapes inside a basic string so an escaped quote does not appear to close it:

fn strip_toml_comment(line: &str) -> &str {
    // ... track quote state; a `#` while `quote == None` starts the comment,
    // a `#` inside a string is literal; `\` escapes the next byte in "..." ...
}

Tests: strip_toml_comment_respects_quoted_strings (comment vs. in-string #, post-string comment, escaped-quote case) and sem_patterns_keep_hash_inside_quoted_match (end-to-end: a # inside a quoted match body survives parsing and still lowers to its template).

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Expose mutable lexer state to lexer action hooks (src/atn/lexer.rs:237)

Fixed in 0bc64e8. Correct — the hooks-based action dispatch built LexerSemCtx from &BaseLexer, so a lexer_action hook could not change the mode stack (unlike the closure custom_action API, which gets &mut BaseLexer), and the accepted token would be followed by lexing in the wrong mode.

LexerSemCtx now holds a LexerRef that is either a shared borrow (speculative lexer_sempred path, where mutation is invalid) or a mutable borrow (committed lexer_action path). dispatch_lexer_action_hook takes &mut BaseLexer and builds the mutable variant via new_mut; the context exposes:

  • push_mode(mode) -> bool
  • pop_mode() -> Option<i32>
  • set_mode(mode) -> bool

These apply on the action path and are inert on the predicate path (returning false/None), so a hook can implement pushMode/popMode/setMode semantics. Read accessors work on both borrows. The SemanticHooks::lexer_action doc now states the mutable capability.

Generated lexers don't route actions to SemanticHooks today (their action dispatch is the static run_action), so this closes the gap for manual next_token_with_semantic_hooks callers that the entry point already advertises.

Test: lexer_action_hook_context_can_change_mode verifies push/pop/set apply through the mutable context and are inert through the shared one.

@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/lexer.rs`:
- Around line 219-233: The documentation for `Lexer::set_mode` is inaccurate
because the `LexerRef::Shared(_)` branch does not emit any warning and only
returns `false`. Update the doc comment on `set_mode` to match the actual
behavior: it is applied only in the `LexerRef::Mut` path, and in the
speculative/predicate context it is silently ignored with a `false` return
value. Keep the wording aligned with the `set_mode` method and the
`LexerRef::Mut` / `LexerRef::Shared` match behavior.
🪄 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: c7d92712-4eb3-4072-9ee7-5399bb6147dc

📥 Commits

Reviewing files that changed from the base of the PR and between 29f9874 and 0bc64e8.

📒 Files selected for processing (4)
  • src/atn/lexer.rs
  • src/bin/antlr4-rust-gen.rs
  • src/lexer.rs
  • src/parser.rs

Comment thread src/lexer.rs
Comment on lines +219 to +233
/// Sets the current lexer mode. Available only from a custom-action hook
/// (the mutable-borrow context); a no-op with a warning path for the
/// speculative predicate context, where mutating lexer state is invalid.
///
/// Returns `true` if the mutation was applied (action context), `false` if
/// it was ignored (predicate context).
pub fn set_mode(&mut self, mode: i32) -> bool {
match &mut self.lexer {
LexerRef::Mut(lexer) => {
lexer.set_mode(mode);
true
}
LexerRef::Shared(_) => 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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc claims a "warning path" that doesn't exist.

The comment says predicate-context calls are "a no-op with a warning path", but Shared(_) just returns false with no warning emitted. This can mislead hook implementers into expecting a diagnostic. Align the doc with the actual behavior (silent false return signals not-applied).

📝 Proposed doc fix
     /// Sets the current lexer mode. Available only from a custom-action hook
-    /// (the mutable-borrow context); a no-op with a warning path for the
-    /// speculative predicate context, where mutating lexer state is invalid.
+    /// (the mutable-borrow context); a silent no-op for the speculative
+    /// predicate context, where mutating lexer state is invalid.
     ///
     /// Returns `true` if the mutation was applied (action context), `false` if
     /// it was ignored (predicate context).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Sets the current lexer mode. Available only from a custom-action hook
/// (the mutable-borrow context); a no-op with a warning path for the
/// speculative predicate context, where mutating lexer state is invalid.
///
/// Returns `true` if the mutation was applied (action context), `false` if
/// it was ignored (predicate context).
pub fn set_mode(&mut self, mode: i32) -> bool {
match &mut self.lexer {
LexerRef::Mut(lexer) => {
lexer.set_mode(mode);
true
}
LexerRef::Shared(_) => false,
}
}
/// Sets the current lexer mode. Available only from a custom-action hook
/// (the mutable-borrow context); a silent no-op for the speculative
/// predicate context, where mutating lexer state is invalid.
///
/// Returns `true` if the mutation was applied (action context), `false` if
/// it was ignored (predicate context).
pub fn set_mode(&mut self, mode: i32) -> bool {
match &mut self.lexer {
LexerRef::Mut(lexer) => {
lexer.set_mode(mode);
true
}
LexerRef::Shared(_) => false,
}
}
🤖 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/lexer.rs` around lines 219 - 233, The documentation for `Lexer::set_mode`
is inaccurate because the `LexerRef::Shared(_)` branch does not emit any warning
and only returns `false`. Update the doc comment on `set_mode` to match the
actual behavior: it is applied only in the `LexerRef::Mut` path, and in the
speculative/predicate context it is silently ignored with a `false` return
value. Keep the wording aligned with the `set_mode` method and the
`LexerRef::Mut` / `LexerRef::Shared` match behavior.

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

methods.insert(mapping.method_name.clone());

P2 Badge Disambiguate colliding typed hook methods

When two helper predicates normalize to the same Rust method name, this set collapses them into one trait method while the dispatch arms for both coordinates call that same method. For example, a grammar with {isFoo()}? and {is_foo()}? maps both helpers through rust_function_name to is_foo, so users cannot implement distinct semantics and one predicate coordinate is evaluated with the wrong hook; please detect such collisions and reject or generate coordinate-specific method names instead of deduping solely by the normalized name.

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

@codex review

Addressed the latest batch through HEAD 0bc64e8:

  • Preserve <fail=...> for all predicates (c8c92e0): a transparent PredicateTemplate::WithFailMessage wrapper carries the message for hook/lookahead/member predicates, not just constant-false. predicate_effective_template keeps it transparent to evaluation, disposition, and generatability. Verified the SemIR builder now emits failure_message: Some(...) where it previously emitted None.
  • Keep unresolved semantic alts as fallbacks (e717e01): the fallback search is now two-pass — resolved-guard alts first, unresolved alts as last resort — so an unresolved rule-call alt neither shadows a later token alt nor becomes unreachable ({p()}? 'a' | x on input in FIRST(x) now selects x).
  • Respect quoted strings in --sem-patterns comments (19d1f85): # inside a quoted TOML string is no longer treated as a comment.
  • Mutable lexer state for action hooks (0bc64e8): LexerSemCtx gives the committed lexer_action path a mutable borrow with push_mode/pop_mode/set_mode; the speculative predicate path stays a shared borrow.

Validation on 0bc64e8: all 7 CI checks green, including the full 357-descriptor Runtime Testsuite (also confirmed with a clean-tree local sweep: 357 passed, 0 failed). clippy -D warnings clean; cargo test --locked green. Each finding has a per-comment reply.

The typed-hook trait declares a fixed `custom_action` method (the action
hook) alongside one method per bare predicate helper. A grammar helper
that normalizes to `custom_action` — e.g. `customAction()` or
`custom_action()` — collided with it, and since Rust traits cannot
overload by arity the generated parser failed to compile (two
`custom_action` methods, E0201) for an otherwise valid hook mapping.

`typed_hook_predicate_method_name` now suffixes a predicate-helper name
that equals the reserved `custom_action` with `_pred`, so the predicate
method and the action hook are distinct. The `sempred` dispatch arm uses
the same disambiguated name, so the mapping stays consistent.

Verified end-to-end: a grammar with `{customAction()}?` under
`--sem-unknown=hook` now generates a trait with both `custom_action_pred`
(predicate) and `custom_action` (action), and the generated crate
compiles (it previously failed with a duplicate-method error).

Tests: `typed_hook_predicate_method_name_avoids_action_hook_collision`
(unit) and `typed_hook_adapter_disambiguates_custom_action_helper`
(renders both distinct methods + the consistent dispatch arm). clippy
clean; full suite green.
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Avoid colliding typed hooks with custom_action (gen.rs:9659)

Fixed in 07a9cbf. Correct — the typed-hook trait declares a fixed custom_action action-hook method plus one method per bare predicate helper, and a helper normalizing to custom_action (customAction() / custom_action()) collided with it. Rust traits can't overload by arity, so the generated parser failed to compile (duplicate method, E0201).

typed_hook_predicate_method_name now suffixes a predicate-helper name that equals the reserved custom_action with _pred, and the sempred dispatch arm uses the same disambiguated name so the mapping stays consistent:

const TYPED_HOOK_ACTION_METHOD: &str = "custom_action";

fn typed_hook_predicate_method_name(helper: &str) -> String {
    let name = rust_function_name(helper);
    if name == TYPED_HOOK_ACTION_METHOD { format!("{name}_pred") } else { name }
}

Verified end-to-end: a grammar with {customAction()}? under --sem-unknown=hook now generates a trait with both custom_action_pred (predicate) and custom_action (action) as distinct methods, and the generated crate compiles — it previously failed with the duplicate-method error.

Tests: typed_hook_predicate_method_name_avoids_action_hook_collision and typed_hook_adapter_disambiguates_custom_action_helper (renders both distinct methods and the consistent dispatch arm).

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

HEAD is now 07a9cbf. Since your c8c92e0 review, addressed:

  • Avoid colliding typed hooks with custom_action (07a9cbf): a bare predicate helper normalizing to custom_action is suffixed _pred, so it no longer collides with the fixed action-hook method. Verified the generated crate compiles (it previously hit E0201).
  • Plus the two findings already replied to: # inside quoted --sem-patterns strings (19d1f85) and mutable lexer state for lexer_action hooks (0bc64e8).

CI on 07a9cbf: clippy + unit tests, Kotlin parity, parse-bench, and CodeQL green; the 357-descriptor Runtime Testsuite is running (the parent 0bc64e8 passed it — confirmed on CI and a clean-tree local sweep at 357/357 — and 07a9cbf is a generator-only change confined to the --sem-unknown=hook typed-hook adapter, so it does not touch the conformance path). clippy -D warnings clean; cargo test --locked green.

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

if slots
.iter()
.any(|slot| matches!(slot, Some(PredicateTemplate::Hook)))
{
return Err(io::Error::new(

P2 Badge Apply lexer coordinate overrides before rejecting hook slots

When a lexer predicate body is matched by a helper/pattern that lowers to hook, this check returns an error before render_lexer gets to apply per-coordinate overrides such as dispose = "assume-false" or "assume-true". In that scenario the manifest/config says the coordinate should be a documented fallback, but generation still fails because the intermediate slot is Hook; apply the coordinate override during slot pairing (as parser predicates do) before deciding that a hook-routed lexer predicate is unhonorable.


if let Some(body) = value
.strip_prefix('"')
.and_then(|body| body.strip_suffix('"'))
{
return unescape_toml_basic_string(body);

P2 Badge Handle TOML literal strings in sem pattern files

--sem-patterns files are TOML-like and strip_toml_comment already treats single-quoted literal strings as quoted, but the scalar parser only removes double quotes. If a user writes valid TOML such as dispose = 'assume-false', lower = 'hook', or a single-quoted match to avoid escaping #/quotes, the quotes remain in the value, causing coordinate dispositions to be rejected or patterns not to match. Strip single-quoted scalars here as literal strings.

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

Two related `--sem-patterns` fixes:

- Apply lexer predicate coordinate overrides before rejecting hook slots.
  `lexer_predicate_templates` rejected a slot that lowered to `Hook` (via a
  helper/pattern) before any per-coordinate override applied, so a
  `dispose = "assume-false"` / `"assume-true"` override on that lexer
  predicate — which the manifest reports as a documented fallback — still
  failed generation. It now applies the `LexerPredicate` coordinate
  override to each position-aligned slot first (mirroring the parser path),
  so an `assume-*` override turns the `Hook` slot into `False`/`True` and
  generation succeeds; a `hook` override still rejects (unsupported for
  generated lexers) and `error` leaves the coordinate uncovered.

- Handle TOML literal (single-quoted) strings in `parse_toml_scalar`.
  `strip_toml_comment` already treats `'...'` as a quoted string, but the
  scalar parser only stripped double quotes, so `dispose = 'assume-false'`,
  `lower = 'hook'`, or a single-quoted `match` kept its quotes and was
  rejected or failed to match. Literal strings are now stripped verbatim
  (no escape processing, per TOML).

Verified end-to-end: a lexer helper lowered to `hook` plus a single-quoted
`dispose = 'assume-false'` lexer-predicate coordinate override now
generates (previously errored on the hook slot), and the manifest reports
the coordinate as `assume-false`.

Tests: `parse_toml_scalar_strips_single_quoted_literals`,
`sem_patterns_accept_single_quoted_dispose`. clippy clean; full suite green.
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Re: Apply lexer coordinate overrides before rejecting hook slots (gen.rs:6164) and Handle TOML literal strings in sem pattern files (gen.rs:1194)

Both fixed in f72c6d9.

Apply lexer coordinate overrides before rejecting hook slots (gen.rs:6164). Correct — lexer_predicate_templates rejected a slot that lowered to Hook before any per-coordinate override applied, so a dispose = "assume-false"/"assume-true" override on that lexer predicate still failed generation even though the manifest reported it as a fallback. The path now applies the LexerPredicate coordinate override to each position-aligned slot first (mirroring the parser path), before the hook-rejection check:

for (slot, (rule_index, pred_index)) in slots.iter_mut().zip(predicates.iter().copied()) {
    if let Some(override_template) = patterns.coordinate_predicate_template(
        SemanticsKind::LexerPredicate, data.rule_names.get(rule_index).map(String::as_str), Some(pred_index),
    ) {
        *slot = override_template;   // assume-* -> Some(False/True); hook -> Some(Hook); error -> None
    }
}

So assume-* turns the Hook slot into False/True and generation succeeds; a hook override still rejects (unsupported for generated lexers) and error leaves the coordinate uncovered for the catch-all.

Handle TOML literal strings (gen.rs:1194). Correct, and a direct consistency gap with the strip_toml_comment fix — strip_toml_comment treats '...' as a quoted string, but parse_toml_scalar only stripped double quotes, so dispose = 'assume-false' (or a single-quoted match/lower) kept its quotes and was rejected/failed to match. parse_toml_scalar now strips single-quoted literals verbatim (no escape processing, per TOML), alongside the existing double-quoted basic-string handling.

Verified end-to-end: a lexer helper lowered to hook plus a single-quoted dispose = 'assume-false' lexer-predicate coordinate override now generates (it previously errored on the hook slot), and semantics.json reports the coordinate as assume-false. Without the override the same grammar still correctly rejects the hook slot.

Tests: parse_toml_scalar_strips_single_quoted_literals, sem_patterns_accept_single_quoted_dispose.

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

HEAD is now f72c6d9, addressing both findings from your last review:

  • Apply lexer coordinate overrides before rejecting hook slots (f72c6d9): lexer_predicate_templates now applies a LexerPredicate coordinate override to each position-aligned slot before the hook-rejection check, so an assume-* override rescues a Hook slot (mirroring the parser path). Verified end-to-end.
  • Handle TOML literal strings (f72c6d9): parse_toml_scalar now strips single-quoted literals, consistent with strip_toml_comment, so dispose = 'assume-false' resolves correctly.

Both are generator-only changes gated on --sem-patterns / --sem-unknown=hook, so they do not touch the conformance path; the parent 07a9cbf passed the full 357-descriptor Runtime Testsuite on CI and a clean-tree local sweep. clippy -D warnings clean; cargo test --locked green (2 new tests). Each finding has a per-comment reply.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: f72c6d9630

ℹ️ About Codex in GitHub

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

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support strategy for target-language semantic predicates and actions

1 participant