feat(codegen)!: accept mutual (indirect) left recursion via hub inlining (#151) - #221
Conversation
Copy/Paste DetectionFound 2 duplication(s) across 4 changed Rust file(s) (threshold: 100 tokens). Show duplicationsFound a 22 line (132 tokens) duplication in the following files:
"parser grammar Delegate;\ndelegated: {isTypeName()}? ID;\n",
)
.expect("delegate grammar should be writable");
fs::write(&tokens, "lexer grammar Tokens;\nID: [a-z]+;\n")
.expect("token grammar should be writable");
let output = run_antlr4_rust_gen(&[
root.as_os_str(),
tokens.as_os_str(),
OsStr::new("-I"),
temp.path().as_os_str(),
OsStr::new("--out-dir"),
out.as_os_str(),
]);
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
utf8(&output.stdout),
utf8(&output.stderr)
);
let parser = fs::read_to_string(out.join("root.rs")).expect("parser should be emitted");
assert!(parser.contains("pub trait RootHooks"), "{parser}");
```rust
---
Found a 17 line (102 tokens) duplication in the following files:
* Starting at line 2978 of tests/antlr4_rust_gen_cli.rs
* Starting at line 3045 of tests/antlr4_rust_gen_cli.rs
```rust
dir.join("L.g4").as_os_str(),
OsStr::new("--sem-patterns"),
dir.join("patterns.toml").as_os_str(),
OsStr::new("--sem-unknown"),
OsStr::new("error"),
OsStr::new("--require-full-semantics"),
OsStr::new("--out-dir"),
out.as_os_str(),
]);
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
utf8(&output.stdout),
utf8(&output.stderr)
);
let lexer = fs::read_to_string(out.join("l.rs")).expect("lexer should be emitted"); |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds a preprocessing pass that detects selected mutual left-recursion cycles, collapses them into direct-recursive hubs, and lets existing precedence rewriting process the result. Unsupported cycles remain unchanged for downstream diagnostics. ChangesMutual left-recursion support
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant GrammarSource
participant SemanticAnalysis
participant MutualRecursionPass
participant LeftCornerAnalysis
participant DirectRecursionRewriter
participant GeneratedParser
GrammarSource->>SemanticAnalysis: provide integrated grammar units
SemanticAnalysis->>MutualRecursionPass: eliminate mutual left recursion
MutualRecursionPass->>LeftCornerAnalysis: compute nullable left-corner cycles
LeftCornerAnalysis-->>MutualRecursionPass: return admissible cycles
MutualRecursionPass-->>SemanticAnalysis: rewrite hubs or preserve declined cycles
SemanticAnalysis->>DirectRecursionRewriter: rewrite direct left recursion
DirectRecursionRewriter-->>GeneratedParser: emit precedence parser
GeneratedParser-->>SemanticAnalysis: produce parse trees for fixture inputs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
📊 Source Code Metrics (this PR vs
|
| File | Cyclomatic | Cognitive | Functions | LLOC | MI |
|---|---|---|---|---|---|
| src/bin_support/grammar/semantics.rs | 654 ⚪ | 511 ⚪ | 124 ⚪ | 890 (main: 889) 🔴 | 0 ⚪ |
| src/bin_support/grammar/mutual_recursion.rs | 312 🆕 | 220 🆕 | 87 🆕 | 424 🆕 | 0 🆕 |
| src/bin_support/grammar/mod.rs | 1 ⚪ | 0 ⚪ | 0 ⚪ | 0 ⚪ | 47.27 (main: 47.67) 🔴 |
Generated by mehen v1.7.0 — the code quality watcher.
|
Claude Code review skipped — usage limit reached.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f2cb374224
ℹ️ 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".
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 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/bin_support/grammar/mutual_recursion.rs`:
- Around line 296-328: The split logic around split_leading_optional must
re-examine produced alternatives until no leading optional cycle-member corner
remains. Update expand_leading_optionals to use a worklist or equivalent
fixpoint loop, reprocessing without_corner and any subsequent outputs before
adding them to expanded; do not allow residual optional recursive corners to
reach substitute_into_hub.
- Around line 125-152: Make the mutual-recursion rewrite decline path
side-effect free by buffering all fresh ID allocations and provenance records
produced during expand_leading_optionals and substitute_into_hub, rather than
mutating the live ids and provenance immediately. Commit the buffered
IDs/provenance only after hub_is_directly_rewritable and all budget checks pass,
before applying the rebuilt rules; ensure declined rewrites leave both the model
and ProvenanceIndex unchanged.
- Around line 383-391: Unify left-corner resolution between inline_satellite and
substitute_into_hub: in src/bin_support/grammar/mutual_recursion.rs:383-391,
consume the corner position returned by left_corner_target instead of selecting
the first RuleCall, and refuse inlining unless that element uses
Quantifier::One. In src/bin_support/grammar/mutual_recursion.rs:503-527, make
left_corner_target return both the resolved target and corner position, and
report or decline cycles when the walk crosses a nullable cycle-member corner;
remove the now-redundant skippable guard.
- Around line 343-363: Preserve source declaration order while collapsing
recursive alternatives in the worklist loop around left_corner_target and
inline_satellite. Replace FIFO appending with in-place expansion at the consumed
alternative’s position, or use equivalent depth-first splicing, so each
satellite expansion remains where its source alternative occurred and nested
inlining cannot reorder precedence.
- Around line 443-446: Update the first-recursive detection around
first_recursive to use first_significant_index, skipping leading Action,
Predicate, and Epsilon elements symmetrically with last_significant. In the
self-loop check, compare last_significant with the computed first_index instead
of literal 0, while preserving the existing None handling.
- Around line 573-582: Update record_hub_provenance to stop recording the hub
rule as its own origin and stop discarding cycle. Extend its inputs or defer the
provenance record until the cycle’s member/satellite owners can be represented,
then record the hub’s synthetic RuleBoundary origin against those real
contributing owners using the surrounding multi-owner provenance semantics.
- Around line 411-419: Update renumber_elements to recurse through
ElementKind::Block values, assigning fresh IDs to each nested block alternative
and recursively renumbering its elements before returning the cloned Alternative
in the inlining flow. Preserve all existing labels, options, commands, syntax,
and spans while ensuring every cloned nested block receives unique element IDs.
- Around line 977-986: Update the test
declines_cycle_without_a_token_consuming_operator so its before snapshot is
rendered from the parsed model before eliminate_mutual_left_recursion runs,
rather than from a separate run that already applies the pass. Keep the after
model and changed assertion, then compare the pre-pass rendering with the
post-pass rendering to verify the model remains untouched.
- Around line 503-527: Update left_corner_target to report nullable rule-call
targets as left-corner cycle members instead of skipping them during traversal.
Ensure substitute_into_hub or its hub_is_directly_rewritable validation declines
rewriting when substitution would leave a nullable cycle-member corner,
preserving the cycle unchanged rather than producing an indirectly
left-recursive model.
In `@src/bin_support/grammar/semantics.rs`:
- Around line 85-94: Update the eliminate_mutual_left_recursion call in the
integration flow to pass the existing root metadata and external-reference
information required by the rewrite. Ensure the transformation preserves
satellites that are declared parser roots or referenced outside rule bodies,
while leaving integrated.roots consistent with the retained entry rules.
In `@tests/antlr4_rust_gen_cli.rs`:
- Around line 2074-2081: Update the assertions in the parser validation test to
avoid interpolating the full parser into failure messages. When reporting
missing or unexpected functions, include only the matched line(s) or a bounded
excerpt of parser output while preserving the existing assertion conditions and
diagnostic context.
In `@tests/fixtures/antlr4-rust-gen/mutual-left-recursion/MutualExpr.g4`:
- Around line 1-31: The mutual-recursion fixture coverage only verifies accepted
cycles and lacks a declined-case contract test. Add a companion grammar fixture
containing an unsupported cycle, such as a three-rule cycle or argument-bearing
recursive call, and assert the CLI exits non-zero with diagnostic code G4A005
while preserving the original rule set.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b05ab171-4563-4127-a2c2-52146e3d5c95
⛔ Files ignored due to path filters (5)
docs/issue-151-mutual-left-recursion-plan.mdis excluded by!**/docs/**docs/mutual-left-recursion-rfc.mdis excluded by!**/docs/**src/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__external_satellite_retained.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__name_cycle_collapsed.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__optional_from_satellite.snapis excluded by!**/*.snap
📒 Files selected for processing (5)
src/bin_support/grammar/mod.rssrc/bin_support/grammar/mutual_recursion.rssrc/bin_support/grammar/semantics.rstests/antlr4_rust_gen_cli.rstests/fixtures/antlr4-rust-gen/mutual-left-recursion/MutualExpr.g4
df3c356 to
62f755c
Compare
Independent validation: LR transform works; hit one unrelated pre-existing blockerI tested this branch end-to-end against Setup note: the branch needed a rebase
The transform is correctReproducing the PR's claim on a minimal grammar with two interacting left-corner cycles ( and the trees are right — left-associativity preserved, both cycles resolved: On the real Roslyn grammar the generator accepts it with zero errors (only the five pre-existing Blocker found (pre-existing, not from this PR): #224Combined grammars allocate token types for inline string literals but emit no lexer rules for them. Roslyn's grammar is combined with ~275 inline literals and one named token, so the generated lexer had 18 tokens while the parser expected ~265 — nothing parsed, with no diagnostic. Filed as #224 with a 4-line repro; it reproduces on a trivial grammar with no left recursion, so it is orthogonal to this PR. ANTLR synthesizes Worked around by mechanically converting to a split lexer/parser grammar with named tokens (265 literals → named tokens, Downstream consequence of inlining — worth documenting more prominentlyThe PR notes satellites lose their context node type as a "known non-goal." On Roslyn that is 16 of 17 cycle members: For a metrics/analysis consumer this is significant, because several semantically distinct constructs collapse into one The good news: the flattened node still carries its distinguishing operator tokens as direct children, so classification by token probe remains possible — That is exactly the strategy mehen's Java walker already uses for grammars-v4 Java's flat Remaining failures are my harness, not the runtimeWith the split grammar I got 12/13 modern-C# constructs parsing ( Summary
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 62f755ce3a
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ecbf73653
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f236b7e7a
ℹ️ 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".
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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/bin_support/grammar/mutual_recursion.rs`:
- Around line 414-418: Update the mutual-recursion expansion logic around the
alternative construction to avoid propagating a hub alternative’s label,
options, and commands to every satellite expansion. Prefer declining the cycle
when any participating alternative is labeled; otherwise preserve the satellite
alternative’s own metadata and ensure generated alternatives cannot contain
duplicate labels.
- Around line 561-571: The substitution budget in substitution_budget only
limits accepted steps, while each step can materialize multiple alternatives.
Update the mutual-recursion guard to bound the combined result.len() +
worklist.len() as well as the existing steps count, preserving the current
budget calculation and ensuring the worklist cannot grow beyond the intended
cap.
- Around line 999-1007: Add a regression test alongside
declines_argument_bearing_recursion that constructs mutually recursive rules
with several satellite branches and alternatives, causing substitution to exceed
the pass’s budget or fail to converge. Run it through run and assert changed is
false, specifically covering the steps > budget bailout without relying on
argument-bearing recursion or ill-founded-cycle rejection.
- Around line 78-99: Wrap the cycle detection and elimination flow around
left_corner_cycles in a bounded outer fixpoint loop, recomputing names, nullable
rules, and cycles after each rewrite. Continue until no cycles remain or an
iteration makes no changes, while preserving the existing conservative gate and
preventing unbounded looping; ensure newly introduced or merged left-corner
cycles are revisited before returning failure.
- Around line 321-327: Update the split logic around the construction of
without_corner to decline the split when removing the corner leaves no
significant elements, including alternatives containing only actions or
predicates. Preserve the existing with_corner/without_corner behavior for
residual alternatives that still contain a significant element, and ensure the
caller treats the declined split as a cycle or otherwise avoids inlining an
epsilon-matching hub alternative.
In `@tests/antlr4_rust_gen_cli.rs`:
- Around line 2290-2304: Replace the handwritten tree_of assertions in the
generated test module with descriptive assert_snapshot! calls that capture the
full generated parse-tree dumps, storing snapshots in a sibling snapshots/
directory. Add #[allow(clippy::disallowed_methods)] to both the generated test
module and temporary generated crate, ensuring the configured insta
dev-dependency is available.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f404a78f-df58-4dbd-9b77-8d7d65cd8d5b
⛔ Files ignored due to path filters (5)
docs/issue-151-mutual-left-recursion-plan.mdis excluded by!**/docs/**docs/mutual-left-recursion-rfc.mdis excluded by!**/docs/**src/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__external_satellite_retained.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__name_cycle_collapsed.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__optional_from_satellite.snapis excluded by!**/*.snap
📒 Files selected for processing (5)
src/bin_support/grammar/mod.rssrc/bin_support/grammar/mutual_recursion.rssrc/bin_support/grammar/semantics.rstests/antlr4_rust_gen_cli.rstests/fixtures/antlr4-rust-gen/mutual-left-recursion/MutualExpr.g4
…ng (#151) ANTLR 4 rewrites *direct* left recursion into a precedence-climbing rule but rejects *mutual* (indirect) left recursion — a left-corner cycle through two or more rules — with error(119), even though the grammar is expressible. This adds a model-level pass that reduces the tractable subclass of those cycles to direct left recursion before the existing direct-recursion rewrite runs, so our generator accepts grammars the reference tool declines. The rewrite is left-corner substitution ("hub inlining"): for each cycle, one member is chosen as the hub; every satellite reachable in left-corner position has its alternatives inlined into the hub until the hub is directly left-recursive. Hub-only satellites are then removed; a satellite referenced from outside the cycle is retained (its body now calls the precedence rule). Leading-optional recursion (C#'s `expr? '..' expr?`) is split into `expr '..' expr? | '..' expr?` before substitution. The pass is gated: it commits only when the rebuilt hub is a shape the direct-recursion classifier accepts (Primary/Prefix/Binary/Suffix). Cycles it cannot reduce — argument-bearing recursion, no token-consuming base case, non-convergent substitution — are left untouched and silent, so the existing ATN-level G4A005 detector still reports them. Correctness is guaranteed by construction: the pass emits exactly the direct-recursion grammar we would feed ANTLR, and that path is already conformance-verified. Validated against dotnet/roslyn's CSharp.Generated.g4 (the motivating case in the issue): our generator now accepts it where it previously failed with error(119)/G4A005, the generated Rust compiles, and parse trees are byte-identical to ANTLR's own runtime on inputs exercising all four cycles (names, types, patterns, records). Full ANTLR runtime testsuite stays green at 357/357 with zero skips; direct-left-recursion handling is untouched. Adds docs/issue-151-mutual-left-recursion-plan.md (design + empirical validation), unit tests with insta snapshots, and an end-to-end CLI regression fixture distilling the tractable Roslyn cycle shapes.
…iner outreach Science-paper-style writeup of the left-corner-substitution pre-pass: problem statement grounded in Roslyn's CSharp.Generated.g4, the algorithm with its gate, the differential correctness argument (byte-identical trees vs ANTLR 4.13.2 on all four cycles), scope boundaries mapped to ANTLR's own error(80)/(122)/(169) refusals, and explicit questions for reviewers (subclass boundary counterexamples, hub selection, label synthesis vs flattened trees, upstream adoption). References the Java tool classes (LeftRecursiveRuleTransformer/Analyzer, LeftRecursionDetector) rather than Rust internals, per the target audience.
Roslyn's VisualBasic.Grammar.g4 (419 rules) replicates the C# findings with the pass unmodified: after repairing pre-existing defects in the published file (unescaped '\=', three duplicate rule definitions, fourteen lexical stubs, three grammar-emitter bugs, seven nullable roots), the sole remaining error is again error(119) on four hub-and-spoke cycles — including a 32-rule expression cycle with 25 one-per-operator satellites and four instances of the leading-optional member-access pattern, plus a VB-specific XML-literals cycle. All four reduce; externally-referenced satellites are retained; trees are byte-identical to ANTLR's runtime on cycle-exercising snippets. New §1.2 (replication + repair log), §3.1 (validation results incl. alternative-growth numbers 30→59), updated §2.4/§4.5/§5.
…r-emitter defects
8f236b7 to
aa03b8f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa03b8f0ed
ℹ️ 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".
…151) Review of #221 found five cases where the pass mutated the grammar and only then discovered it could not finish, silently dropping what it could not carry. All five trace to one structural mistake: the rewrite computed *which* rule was the left corner in one place and *where* that corner sat in another, and it committed edits before validating the result. Verified defects, each now a clean decline or a correct rewrite: - `<assoc=right>` on a satellite alternative was dropped, so `expr : power | ID; power : <assoc=right> expr '^' expr` produced a LEFT-associative tree where ANTLR produces a right-associative one. Spliced alternatives now inherit the satellite alternative's options, and the emitted tree matches ANTLR's byte-for-byte. - A `*`-quantified corner (`a : b* 'x'; b : a 'b'`) was replaced by exactly one satellite body, deleting the closure and changing the accepted language. - A corner behind a nullable prefix (`a : n b`, `n :`) was resolved to `b` by the decision but spliced at `n` by the edit. - Element labels, rule arguments and rule-level `@init`/`@after`/`catch` bodies on satellites were discarded when the rule was removed. - Lexer grammars were routed through the parser-only precedence rewrite, surfacing as "unsupported embedded lexer action" naming an action the grammar never declared. The pass is now split into `plan_cycle` (proves admissibility against the untouched model; allocates nothing) and `apply_plan` (mechanical, cannot fail), with the preconditions enumerated in one place. A declined cycle is left bit-for-bit unchanged — no IDs consumed, no provenance written — which is asserted directly. Substitution splices in place rather than appending to a worklist, so declared alternative order (and therefore precedence) is preserved; leading optionals expand to a fixpoint; and spliced element trees are renumbered recursively so no ID is shared between live nodes. Tests: 20 unit tests, 13 of them decline paths, one per precondition. The previous "model is untouched" assertion compared two post-pass renderings and so could never fail; it now renders before invoking the pass. Adds a CLI fixture asserting that a declined cycle still reports G4A005 naming the original rules and emits no parser. Failure messages print matching lines instead of interpolating the whole generated parser. Roslyn C# (7 snippets) and Visual Basic (4 snippets) parse trees remain byte-identical to ANTLR 4.13.2's runtime; full unit suite and clippy green. Left-recursive *lexer* rules crash or misdiagnose on main independently of this pass; filed as #236 rather than papered over here.
Review round addressed — 33 threads, 5 confirmed defects, 1 spun outThanks both; this review found a genuine architectural mistake, not a list of nits. I reproduced every actionable claim before changing anything. Five were real, and they shared one root cause: the pass mutated the grammar and only then checked whether it could finish, and it derived which rule was the left corner separately from where that corner sat. Confirmed defects (each reproduced, then fixed)
The structural fixThe pass is now decide, then act:
So a declined cycle is left bit-for-bit unchanged (no IDs consumed, no provenance written), which Tests20 unit tests, 13 of them decline paths, one per precondition. The old "model is untouched" assertion compared two post-pass renderings and could never fail (thanks @coderabbitai) — the helper now renders before invoking the pass, and all 13 use it. New CLI fixture Validation
Spun out, not papered overSkipping lexer units revealed that a left-recursive lexer rule stack-overflows the ATN builder — and does so on pristine Declined, with reasonsTwo suggestions I did not take: snapshotting the generated Rust (it churns with unrelated codegen changes; I pin the parse tree instead, cross-validated against ANTLR, and snapshot the model-level rewrite separately), and a budget-exhaustion test (the loop's progress condition is now structural, so I cannot construct a non-convergent case — such a test would pin the bound, not the behaviour). Both are answered in-thread. The net effect is that the subclass this PR accepts is narrower and provable rather than broad and occasionally wrong, which is what the issue asked for: never accept-and-miscompile. |
|
Tip For best results, initiate chat on the files or code changes.
The strengthened decline-path coverage—especially asserting no ID/provenance changes and retaining 🐇 ✅ ✏️ Learnings added
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
Bring both documents in line with the precondition-first implementation: the decline list is now enumerated per precondition (bare corners, nullable-prefix ambiguity, satellite rule-level state, authored alternative labels, lexer grammars) rather than the earlier three-item sketch, and the RFC records the five adversarial shapes that located the boundary — including the <assoc=right> case that produced a wrong-associativity tree — as regression tests rather than open risks.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/bin_support/grammar/mutual_recursion.rs`:
- Around line 383-404: Update split_optional to return
Option<Vec<PlannedAlternative>> and decline the split when the candidate carries
a label or other duplicated alternative attributes. Also reject the split when
removing the optional element leaves no significant grammar element, preventing
an epsilon-only branch from making a non-nullable hub appear nullable; update
its callers to handle the declined cycle.
- Around line 559-599: Remove the unused enumeration from the alternatives
iterator in the plan-building flow: update the closure around Alternative
construction to accept only each planned alternative, and delete the
corresponding let _ = index statement. Preserve all existing identity,
provenance, element, and source-attribute handling.
- Around line 694-726: Update removable_satellites so reachability includes
references from the hub’s planned alternatives, including verbatim and nested
block content, instead of unconditionally skipping hub_id. Traverse the planned
hub element lists alongside other surviving rules, while still excluding
removable satellite rules and preserving the existing fixpoint removal behavior.
- Around line 328-379: Update planned_corner so every substitutable corner
requires element.options.is_empty(), matching classify_corner’s bare-corner
predicate. Apply this condition to both the optional-split branch and the
regular satellite path, preventing options-bearing elements from being spliced
or having options discarded; preferably reuse the shared predicate if available.
- Around line 111-126: Ensure plan_cycle returns None when it produces no
substitutions or removable cycle members, rather than returning a no-op plan.
Update the plan construction around removable and the planned alternatives so at
least one rewrite is required before apply_plan is called; preserve existing
valid plans and let the outer loop exit when no progress is possible.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e48fd967-8b8e-45f6-a7ea-60f22a94a6b1
⛔ Files ignored due to path filters (6)
src/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__assoc_right_preserved.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__consecutive_optionals.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__declared_order_preserved.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__external_satellite_retained.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__name_cycle_collapsed.snapis excluded by!**/*.snapsrc/bin_support/grammar/snapshots/antlr4_rust_gen__grammar__mutual_recursion__tests__optional_from_satellite.snapis excluded by!**/*.snap
📒 Files selected for processing (3)
src/bin_support/grammar/mutual_recursion.rstests/antlr4_rust_gen_cli.rstests/fixtures/antlr4-rust-gen/mutual-left-recursion/DeclinedCycle.g4
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 793b3ae02e
ℹ️ 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".
Six confirmed defects, one shared theme — decisions and bookkeeping must both read the *planned* model, not the original: - A plan that made zero substitution steps was re-selected verbatim by the driver loop forever (hang on a block-shaped corner). Decline when no step was taken; the cycle is out of reach, G4A005 reports it. - Satellite removability was judged against the original hub body, so a satellite still referenced by the planned hub (second occurrence in a spliced alternative, or an unspliced verbatim alternative) was deleted, leaving a dangling rule call that panicked the ATN build. Removability now scans the planned elements plus every surviving rule. - The caller's #label (and lexer commands) were replaced by the spliced satellite's attribution, silently deleting the labelled context class. Attribution is now split: label/commands from the hub alternative, options (<assoc=...>) from the satellite alternative that supplied the operator, satellite winning name conflicts. - The admissibility gate skipped leading epsilon-only elements while the downstream classifier keys on the literal first element, so a predicate-prefixed satellite alternative committed a still-recursive hub and G4A005 then named the wrong rule set. The gate now mirrors the literal-first reading and backstops every primary alternative with a left-corner closure check. - Nongreedy `X??` corners were split with present-first order, inverting the authored preference; only greedy optionals split now. - Splicing or splitting away a corner left `$rule.attr` references in surviving actions dangling; both paths now decline when any remaining action or predicate references the removed rule by name. The blanket nullable-hub decline added while restructuring is dropped again: Roslyn's `pattern` hub is nullable (all-optional recursive_pattern) yet ANTLR accepts the collapsed grammar — the shapes nullability could smuggle in are already declined by the gate. Verified: 29 unit tests (9 new incl. termination, retention snapshots, label preservation, decline paths); Roslyn C# 7/7 and VB 4/4 parse trees byte-identical to ANTLR 4.13.2 on the transformed grammars; transformed DanglingSuffix/DanglingVerbatim2/CallerAltLabel oracled against ANTLR directly; 357/357 conformance descriptors.
Extend the decline lists in the plan and the RFC with the round-3 preconditions (nongreedy corners, dangling action references, zero-step plans, option-bearing corners), sharpen the removability and attribution descriptions to the planned-model reading, and add the second adversarial-shape table to RFC §4.1 — including the one reviewer claim the ANTLR oracle refuted (duplicate #label on split products is legal).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6106e72040
ℹ️ 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".
…splices A chained splice overwrote the planned alternative's option source with the newest satellite: for `e : a | ID; a : <assoc=right> b '^' e; b : e;` the alias splice of `b` replaced `a`'s `<assoc=right>` with `b`'s empty options, and `x^y^z` parsed left-associatively where ANTLR parses the faithful inlined grammar right-associatively. Options of every alternative merged into a position apply to the flattened result, so the plan now carries the accumulated option set itself (seeded from the hub alternative, unioned per splice, shared by both split products) instead of a single source-alternative id. Two alternatives declaring the same option with different values along one chain is genuinely ambiguous — the cycle declines rather than picking a winner. apply_plan simplifies to reading the accumulated set. Verified: ChainAssoc reproducer now parses x^y^z right-associatively, byte-matching ANTLR 4.13.2 on the transformed grammar; 31 unit tests (2 new: carried-option snapshot, conflicting-chain decline); existing snapshots unchanged; Roslyn C# 7/7 + VB 4/4 byte-identical; 17 prior reproducers unchanged; 357/357 conformance.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb69af7100
ℹ️ 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".
…nce capture, and parameterized hubs Three review round-5 findings, all reproduced against --actions embedded before fixing: - A satellite whose embedded action binds its own rule context ($ctx, $text, $start, $stop, or the rule's own name) was transplanted into the hub, silently rebinding those references to the hub's context (the satellite's `let _r = $ctx;` landed inside the hub's body). Such satellites now decline; actions bound only to their own alternative's element labels still splice (the element travels with the action). - The label-collision check covered explicit labels only, but implicit references bind by occurrence within their alternative: splicing a satellite body containing `ID` ahead of a caller action referencing $ID captured the reference (the caller's $ID resolved to the satellite's already-matched token). Splices now decline when either side's actions name a token, rule or label the other side introduces; inert duplicate occurrences without action references remain fine. - A parameterized hub (`e[i32 x]`) was rewritten even though every in-cycle corner is bare and thus omits the required arguments — deleting the invalid call before semantic call validation could see it and default-initializing the parameter. Parameterized hubs now decline before planning, and the genuine G4S043 "missing arguments on rule reference" diagnostic surfaces on the untouched grammar. Verified: 35 unit tests (4 new, incl. an own-label-action guard against over-declining); CtxAction/ImplicitTok decline with G4A005 naming both rules, ParamHub reports G4S043; 18 prior reproducers unchanged; Roslyn C# 7/7 + VB 4/4 byte-identical to ANTLR 4.13.2; 357/357 conformance.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b18ebff1e6
ℹ️ 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".
…validation, non-nullable recursive tails
Three review round-6 findings, each reproduced before fixing:
- Satellite embedded actions/predicates now decline entirely, replacing
the round-5 context-reference allowlist. Semantic bodies are owned by
their rule and alternative: the semantic-context parameter and $ctx
mean the satellite's context, and the embedded-action pipeline
resolves $-references against the enclosing alternative's identity —
even the supposedly safe own-label case (`s : e '+' x=ID {$x.text}`)
failed end-to-end ("cannot translate $x") because the spliced
alternative carries the hub's span. Filtering bodies for the
references that break is target-language-specific and incomplete;
ownership is the invariant. Caller-side actions are unaffected (the
rebuilt hub alternative keeps its own span identity; verified under
--actions embedded).
- Symbol-conflict validation now reads a snapshot of the units taken
before the mutual-recursion pass runs. Deleting a hub-only satellite
used to hide a conflict involving its name: `e returns [i32 s]` vs
rule `s` generated successfully; it now reports G4S057 against the
authored grammar.
- The admissibility gate requires every recursive alternative's tail to
be non-nullable. `e : s | ID; s : e n; n : ;` used to commit the
splice and then fail with G4A002 describing the *transformed* rule —
a decide-before-act violation; it now declines untouched and G4A005
names the authored cycle [e, s].
Verified: 36 unit tests + new CLI fixture (ReturnsClash → G4S057);
all six round-5/6 reproducers land on their intended diagnostics; 18
prior reproducers unchanged; Roslyn C# 7/7 + VB 4/4 byte-identical;
357/357 conformance.
Closes #151.
What
ANTLR 4 rewrites direct left recursion (
e : e '+' e | INT) into a precedence-climbing rule, but rejects mutual (indirect) left recursion — a left-corner cycle through two or more rules — witherror(119), even though the grammar is expressible in ANTLR syntax. This PR teaches our generator to accept the tractable subclass of those cycles, reducing them to direct left recursion on the model before the existing direct-recursion rewrite runs.The motivating real-world case (from the issue) is
dotnet/roslyn'sCSharp.Generated.g4— the C# compiler team's own grammar, whose only remaining blocker (once its six empty rules are corrected) iserror(119)on four cycles:type/array_type/nullable_type/pointer_type,name/qualified_name, a 13-ruleexpressioncycle, andpattern/binary_pattern.How — left-corner substitution ("hub inlining")
For each left-corner cycle, one member is chosen as the hub; every satellite reachable in left-corner position from the hub has its alternatives inlined into the hub until the hub is directly left-recursive. Then:
binary_pattern,qualified_name, …);array_type, used byarray_creation_expression);expression? '..' expression?) is split intoexpr '..' expr? | '..' expr?before substitution — a union-preserving rewrite ANTLR accepts.The hub then flows into the unchanged
rewrite_immediate_left_recursion, which produces the precedence-climbingprimary (operator)*form.Correctness by construction
The pass is gated: it commits a rewrite only when the rebuilt hub is a shape the direct-recursion classifier already accepts (every alternative Primary/Prefix/Binary/Suffix). Anything it cannot reduce — argument-bearing recursion, no token-consuming base case, non-convergent substitution — is left untouched and silent, so the existing ATN-level
G4A005detector reports it exactly as before. The pass therefore either emits a direct-recursion grammar the conformance-verified path accepts, or it changes nothing.Because the grammar we emit is exactly the one we would hand to ANTLR, tree-equality is differentially testable against ANTLR's own runtime — which is how it was validated.
Validation
CSharp.Generated.g4(previouslyerror(119)/G4A005); the generated Rust compiles; parse trees are byte-identical to ANTLR 4.13.2's runtime on inputs exercising all four cycles (records,switchexpressions,is not/and/orpatterns, array & nullable types, dotted names, chained invocation/element access). The only grammar change needed is the "minimal lexer adjustment" the issue describes (supply the 9 lexer token rules; parser-only grammar).G4A005diagnostic (the pass correctly declines them).MutualExpr.g4) that generates, compiles, and asserts byte-identical precedence trees.Scope / acceptance criteria
This satisfies staged criteria 1 (detect+diagnose, already shipped via G4A005) and 2 (accept a proven subclass with a correct parser + full parity), and documents which shapes are supported vs declined (criterion 3) in
docs/issue-151-mutual-left-recursion-plan.md.Known non-goal (documented): inlined satellites lose their per-rule context node type (e.g. no
BinaryExpressionContext) — the tree matches ANTLR's from the transformed grammar. Recovering typed satellite nodes needs synthesized alternative labels (ANTLR forbids mixing labeled/unlabeled alts per rule), deferred as a follow-up. A pre-existing runtime prediction difference on C#varlocal-declaration statements is unrelated to this change (reproduced with the hand-inlined grammar where this pass is a no-op) and is out of scope.Summary by CodeRabbit
New Features
Bug Fixes
Tests