Skip to content

Fix GLR nullable reduce self-loops#94

Merged
ehwan merged 5 commits into
mainfrom
issue91
Jun 30, 2026
Merged

Fix GLR nullable reduce self-loops#94
ehwan merged 5 commits into
mainfrom
issue91

Conversation

@ehwan

@ehwan ehwan commented Jun 30, 2026

Copy link
Copy Markdown
Owner

close #91

  • Expand safe auto-generated P? occurrences when their empty branch would create a zero-consuming GLR reduce self-loop.
  • Emit glr_optional_expanded info diagnostics showing the original production and generated replacement productions.
  • Reject remaining zero-consuming GLR nullable cycles at generation time with actionable errors for observed P?, P*, and user-defined nullable productions.
  • Wire the new error through CLI/buildscript, derive macro, and LSP diagnostics.
  • Document the GLR nullable-cycle normalization/error behavior and add regression tests.

@ehwan ehwan self-assigned this Jun 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces automatic expansion of safe, unobserved optional (P?) occurrences in GLR grammars to prevent zero-consuming reduce cycles, and reports unexpandable nullable cycles as generation errors. The changes span the build script, derive macro, LSP diagnostics, and parser. The review comments suggest two valuable optimizations: first, batching the expansion of optional helpers in expand_glr_epsilon_self_loop_optionals to avoid repeatedly rebuilding the state machine; second, deduplicating the error formatting logic for ParseError::GlrNullableReduceCycle in the build script.

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 on lines +654 to +686
let mut rewrite_target = None;
'states: for (state_idx, state) in states.iter().enumerate() {
for reduce_rules in state.reduce_map.values() {
for &rule_idx in reduce_rules {
let Some((nonterm_idx, local_rule_idx)) = self.rule_id_to_indices(rule_idx)
else {
continue;
};
if !self
.is_optional_epsilon_self_loop_candidate(nonterm_idx, local_rule_idx)
{
continue;
}
if state
.shift_goto_map_nonterm
.iter()
.any(|(nonterm, target)| {
*nonterm == nonterm_idx && *target == state_idx
})
{
rewrite_target = Some(nonterm_idx);
break 'states;
}
}
}
}

let Some(optional_idx) = rewrite_target else {
break;
};
if !self.expand_optional_occurrences(optional_idx) {
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation of expand_glr_epsilon_self_loop_optionals runs a loop up to self.nonterminals.len() times, and in each iteration, it builds the entire LR/LALR state machine just to find and expand a single optional helper. For larger grammars, building the state machine is a very expensive operation.\n\nWe can significantly optimize this by collecting all candidate optional helpers that participate in a self-loop in the current state machine, and expanding all of them in a single pass before rebuilding the state machine. This reduces the number of state machine builds from O(K) (where K is the number of self-looping optionals) to O(1) in almost all cases.

            let mut rewrite_targets = BTreeSet::new();\n            for (state_idx, state) in states.iter().enumerate() {\n                for reduce_rules in state.reduce_map.values() {\n                    for &rule_idx in reduce_rules {\n                        let Some((nonterm_idx, local_rule_idx)) = self.rule_id_to_indices(rule_idx)\n                        else {\n                            continue\n                        };\n                        if !self\n                            .is_optional_epsilon_self_loop_candidate(nonterm_idx, local_rule_idx)\n                        {\n                            continue\n                        }\n                        if state\n                            .shift_goto_map_nonterm\n                            .iter\n                            .any(|(nonterm, target)| {\n                                *nonterm == nonterm_idx && *target == state_idx\n                            })\n                        {\n                            rewrite_targets.insert(nonterm_idx);\n                        }\n                    }\n                }\n            }\n\n            if rewrite_targets.is_empty() {\n                break;\n            }\n            let mut changed_any = false;\n            for optional_idx in rewrite_targets {\n                if self.expand_optional_occurrences(optional_idx) {\n                    changed_any = true;\n                }\n            }\n            if !changed_any {\n                break;\n            }

Comment on lines +933 to +953
ParseError::GlrNullableReduceCycle {
location,
nullable_rule,
state,
reason,
help,
} => {
let range = span_manager.get_byterange(&location).unwrap_or(0..0);
Diagnostic::error()
.with_message("GLR nullable reduce cycle cannot be expanded safely")
.with_labels(vec![Label::primary(file_id, range).with_message(
"this nullable production participates in a zero-consuming GLR cycle",
)])
.with_notes(vec![
format!(
"reducing `{nullable_rule}` in state {state} returns to the same state without consuming input"
),
format!("why RustyLR cannot rewrite it automatically: {reason}"),
format!("how to fix it: {help}"),
])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This error formatting block for ParseError::GlrNullableReduceCycle is identical to the one defined at lines 892-912. To improve maintainability and avoid code duplication, consider extracting this formatting logic into a helper function or method on Builder (e.g., fn format_glr_nullable_cycle_error(...) -> Diagnostic<usize>).

@ehwan ehwan merged commit f7022b7 into main Jun 30, 2026
1 check passed
@ehwan ehwan deleted the issue91 branch June 30, 2026 00:30
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.

Stack overflow on recursive grammar

1 participant