Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SYNTAX.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ When a diagnostic is emitted, RustyLR will suggest the exact `%allow <name>(<tar
- `terminals_merged`: Info about terminals being merged into a terminal class.
- `redundant_rule_removed`: Info about a production rule being optimized out due to terminal class rules merging.
- `unit_production_eliminated`: Info about a single non-terminal rule being replaced directly by its child rule.
- `glr_optional_expanded`: Info about an auto-generated optional pattern being expanded to avoid a zero-consuming GLR reduce cycle.
- `reduce_reduce_conflict_resolved`: Info about a reduce/reduce conflict resolved via precedence.
- `shift_reduce_conflict_resolved`: Info about a shift/reduce conflict resolved via precedence.
- `shift_reduce_conflict_glr`: Info about a shift/reduce conflict that splits branches in GLR mode.
Expand Down
1 change: 1 addition & 0 deletions USING_RUSTYLR_WITH_AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ While implementing:
When debugging:
- Run `rustylr --state <grammar-file>` to inspect parser states.
- Read conflict diagnostics before switching to GLR.
- Remember that RustyLR may expand safe, unobserved `P?` occurrences in GLR grammars when the generated optional helper would otherwise create a zero-consuming reduce cycle; unexpandable nullable cycles are reported as generation errors with rewrite guidance.
- Check `context.expected_token()` and `context.can_feed(&token)` for interactive or editor-facing parsers.
- Use `Debug` on a context for parser stack state and user data; in GLR mode, inspect the branch-grouped output. Use `to_tree_list()`/`to_tree_lists()` when syntax-tree inspection is needed.
- Confirm the generated parser compiles and creates contexts with a compatible `rusty_lr` runtime version. Major/minor generator/runtime mismatches panic during context creation.
Expand Down
2 changes: 2 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Generated contexts implement `Clone` when their runtime storage and user data sa

Context `Debug` output reports parser stack state and user data; GLR output is grouped by active branch. Use the explicit tree APIs for syntax-tree inspection when the `tree` feature is enabled.

In GLR grammars, RustyLR may expand safe, unobserved auto-generated `P?` occurrences when their empty branch would otherwise form a zero-consuming reduce cycle. Unexpandable nullable cycles are reported as generation errors with rewrite guidance.

Important files:
- `README.md`: Main quick start and examples.
- `USING_RUSTYLR_WITH_AI.md`: Short implementation guide for AI coding agents using RustyLR in another Rust project.
Expand Down
95 changes: 92 additions & 3 deletions rusty_lr_buildscript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,7 @@ impl Builder {
.with_labels(vec![Label::primary(file_id, range)
.with_message("unknown diagnostic name")])
.with_notes(vec![
"valid diagnostic names: nonterm_unreachable, unused_nonterm_data, nonterm_unproductive, unused_terminals, terminals_merged, redundant_rule_removed, unit_production_eliminated, reduce_reduce_conflict_resolved, shift_reduce_conflict_resolved, shift_reduce_conflict_glr, reduce_reduce_conflict_glr".to_string(),
"valid diagnostic names: nonterm_unreachable, unused_nonterm_data, nonterm_unproductive, unused_terminals, terminals_merged, redundant_rule_removed, unit_production_eliminated, glr_optional_expanded, reduce_reduce_conflict_resolved, shift_reduce_conflict_resolved, shift_reduce_conflict_glr, reduce_reduce_conflict_glr".to_string(),
])
}

Expand All @@ -889,6 +889,27 @@ impl Builder {
"refer to https://github.com/ehwan/RustyLR/blob/main/SYNTAX.md#substitution-errors".to_string(),
])
}
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}"),
])
}
};

let writer = self.stream();
Expand All @@ -905,7 +926,49 @@ impl Builder {
grammar.optimize(25);
}

grammar.builder = grammar.create_builder();
grammar.builder = match grammar.create_builder() {
Ok(builder) => builder,
Err(err) => {
let diag = match err {
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}"),
])
}
Comment on lines +933 to +953

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>).

err => {
let range = err
.locations()
.first()
.and_then(|loc| span_manager.get_byterange(loc))
.unwrap_or(0..0);
Diagnostic::error()
.with_message(err.short_message())
.with_labels(vec![Label::primary(file_id, range)])
}
};
let writer = self.stream();
let config = codespan_reporting::term::Config::default();
term::emit_to_write_style(&mut writer.lock(), &config, &files, &diag)
.expect("Failed to write to stderr");
return Err(diag.message);
}
};
let diags_collector = grammar.build_grammar();

let mut conflict_errors = Vec::new();
Expand Down Expand Up @@ -1153,6 +1216,31 @@ impl Builder {
.to_string(),
])
}
rusty_lr_parser::error::Info::GlrOptionalExpanded {
nonterm_name,
rule_location,
before,
after,
} => {
let nonterm_range = span_manager
.get_byterange(&nonterm_name.location())
.unwrap_or(0..0);
let rule_range = span_manager.get_byterange(rule_location).unwrap_or(0..0);
let mut notes = vec![
"This GLR production was expanded because an auto-generated optional empty branch formed a zero-consuming reduce cycle.".to_string(),
format!("before: {before}"),
];
notes.extend(after.iter().map(|rule| format!("after: {rule}")));
Diagnostic::note()
.with_message("Production rule expanded")
.with_labels(vec![
Label::primary(file_id, rule_range)
.with_message("production expanded here"),
Label::secondary(file_id, nonterm_range)
.with_message("left-hand side defined here"),
])
.with_notes(notes)
}
rusty_lr_parser::error::Info::ReduceReduceConflictResolved {
max_priority,
reduce_rules,
Expand Down Expand Up @@ -1458,7 +1546,8 @@ impl Builder {
let should_print = match info_variant {
rusty_lr_parser::error::Info::TerminalsMerged { .. }
| rusty_lr_parser::error::Info::RedundantRuleRemoved { .. }
| rusty_lr_parser::error::Info::UnitProductionEliminated { .. } => true,
| rusty_lr_parser::error::Info::UnitProductionEliminated { .. }
| rusty_lr_parser::error::Info::GlrOptionalExpanded { .. } => true,
_ => false,
};
if should_print {
Expand Down
5 changes: 4 additions & 1 deletion rusty_lr_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ pub fn lr1(input: TokenStream) -> TokenStream {
if grammar.optimize {
grammar.optimize(15);
}
grammar.builder = grammar.create_builder();
grammar.builder = match grammar.create_builder() {
Ok(builder) => builder,
Err(e) => return e.to_compile_error(&span_manager).into(),
};
let diags = grammar.build_grammar();
if !grammar.glr {
if let Some(((term, shift_rules, _), reduce_rules)) =
Expand Down
49 changes: 48 additions & 1 deletion rusty_lr_executable/src/lsp/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use lsp_types::{Diagnostic, DiagnosticSeverity, Range};
use proc_macro2::{Spacing, TokenStream, TokenTree};
use rusty_lr_core::{TerminalSymbol, production::LR0ItemRef};
use rusty_lr_parser::error::ParseError;
use rusty_lr_parser::grammar::Grammar;
use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};
Expand Down Expand Up @@ -233,7 +234,53 @@ pub fn compile_and_get_diagnostics(content: &str) -> Vec<Diagnostic> {
if grammar.optimize {
grammar.optimize(25);
}
grammar.builder = grammar.create_builder();
grammar.builder = match grammar.create_builder() {
Ok(builder) => builder,
Err(err) => {
match err {
ParseError::GlrNullableReduceCycle {
location,
nullable_rule,
state,
reason,
help,
} => {
let range = span_manager.get_byterange(&location).unwrap_or(0..0);
diagnostics.push(Diagnostic {
range: range_to_lsp_range(content, range),
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some("rusty_lr".to_string()),
message: format!(
"GLR nullable reduce cycle cannot be expanded safely: reducing `{nullable_rule}` in state {state} returns to the same state without consuming input. Reason: {reason}. Fix: {help}"
),
related_information: None,
tags: None,
data: None,
});
}
err => {
let msg = err.short_message();
for loc in err.locations() {
let range = span_manager.get_byterange(&loc).unwrap_or(0..0);
diagnostics.push(Diagnostic {
range: range_to_lsp_range(content, range),
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some("rusty_lr".to_string()),
message: msg.clone(),
related_information: None,
tags: None,
data: None,
});
}
}
}
return diagnostics;
}
};

// 7. Verify Shift/Reduce and Reduce/Reduce conflicts in non-GLR mode
let diags_collector = grammar.build_grammar();
Expand Down
31 changes: 31 additions & 0 deletions rusty_lr_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ pub enum ParseError {

/// unknown diagnostic name allowed
InvalidAllowDiagnostic { location: Location, name: String },

/// a nullable production can be reduced forever in GLR without consuming input
GlrNullableReduceCycle {
location: Location,
nullable_rule: String,
state: usize,
reason: String,
help: String,
},
}
impl ArgError {
pub fn to_compile_error(
Expand Down Expand Up @@ -282,6 +291,7 @@ impl ParseError {
ParseError::CircularDependency { location, .. } => vec![*location],
ParseError::MaxSubstitutionDepthExceeded { location, .. } => vec![*location],
ParseError::InvalidAllowDiagnostic { location, .. } => vec![*location],
ParseError::GlrNullableReduceCycle { location, .. } => vec![*location],
}
}

Expand Down Expand Up @@ -358,6 +368,16 @@ impl ParseError {
ParseError::InvalidAllowDiagnostic { name, .. } => {
format!("unknown diagnostic name: `{}`", name)
}
ParseError::GlrNullableReduceCycle {
nullable_rule,
reason,
..
} => {
format!(
"GLR nullable reduce cycle cannot be expanded safely: `{}` ({})",
nullable_rule, reason
)
}
}
}
}
Expand Down Expand Up @@ -543,6 +563,12 @@ pub enum Info {
nonterm_name: Located<String>,
rule_location: Location,
},
GlrOptionalExpanded {
nonterm_name: Located<String>,
rule_location: Location,
before: String,
after: Vec<String>,
},
ReduceReduceConflictResolved {
max_priority: usize,
reduce_rules: Vec<usize>,
Expand Down Expand Up @@ -578,6 +604,7 @@ impl Info {
Info::TerminalsMerged { .. } => "terminals_merged",
Info::RedundantRuleRemoved { .. } => "redundant_rule_removed",
Info::UnitProductionEliminated { .. } => "unit_production_eliminated",
Info::GlrOptionalExpanded { .. } => "glr_optional_expanded",
Info::ReduceReduceConflictResolved { .. } => "reduce_reduce_conflict_resolved",
Info::ShiftReduceConflictResolvedShift { .. } => "shift_reduce_conflict_resolved",
Info::ShiftReduceConflictResolvedReduce { .. } => "shift_reduce_conflict_resolved",
Expand All @@ -592,6 +619,10 @@ impl Info {
let name = nonterm_name.value();
format!("%allow {}({});", self.name(), name)
}
Info::GlrOptionalExpanded { nonterm_name, .. } => {
let name = nonterm_name.value();
format!("%allow {}({});", self.name(), name)
}
Info::RedundantRuleRemoved { rule_location } => {
let mut name = String::new();
for nonterm in &grammar.nonterminals {
Expand Down
Loading
Loading