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
2 changes: 1 addition & 1 deletion GLR.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ A GLR parser will execute all branches concurrently, tracking all possible parse

## Advanced Dynamic Ambiguity Resolution

RustyLR allows you to resolve grammar conflicts and prune parsing branches dynamically within your reduce actions. This is an advanced GLR escape hatch; for ordinary operator precedence, prefer `%left`, `%right`, `%precedence`, and `%prec`.
RustyLR allows you to resolve grammar conflicts and prune parsing branches dynamically within your reduce actions. This is an advanced GLR escape hatch; for ordinary operator precedence, prefer `%left`, `%right`, `%precedence`, and `%prec`. The reserved `error` terminal can participate in precedence declarations when recovery productions need deterministic conflict resolution.

### 1. Pruning Paths via `Err`
If a reduce action evaluates to a semantic error and returns `Err`, the parser immediately discards that active parsing branch. By checking the lookahead token or parsing context, you can selectively fail unwanted paths.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ For more details, see [GLR.md](GLR.md).
RustyLR provides multiple tools to resolve grammar ambiguities and handle parsing failures:
- **Panic-Mode Error Recovery:** Use the special `error` terminal to catch and recover from syntax errors. Unlike Bison's blocking loop-based recovery, RustyLR incrementally discards and merges subsequent unexpected terminal symbols into the `error` terminal's location span on each `feed()`, enabling reactive stream parsing and accurate diagnostic spans.
- **Operator Precedence:** Disambiguate expressions with `%left`, `%right`, and `%precedence` directives.
- **Recovery Precedence:** The reserved `error` terminal can also appear in precedence declarations when recovery productions need explicit shift/reduce conflict resolution.
- **Advanced Reduce Production Priority:** Resolve reduce/reduce conflicts with `%dprec` when precedence declarations are not enough.
- **Runtime Error Propagation:** Return custom `Err` payloads from reduce actions to signal semantic or parsing errors.

Expand Down
1 change: 1 addition & 0 deletions SYNTAX.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ Used to resolve shift/reduce conflicts in expressions. The precedence of termina
- **`%left`**: Declares left-associative operators (e.g., `a + b + c` parses as `(a + b) + c`).
- **`%right`**: Declares right-associative operators (e.g., `a ^ b ^ c` parses as `a ^ (b ^ c)`).
- **`%precedence`**: Declares precedence without associativity. Banning chained usage of the operator without parentheses.
- The reserved `error` terminal may be listed in precedence declarations to resolve conflicts introduced by panic-mode recovery productions.

When a conflict arises between shifting a terminal and reducing a production rule:
1. The parser compares the precedence of the lookahead terminal to the precedence of the production's *operator*.
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 @@ -123,6 +123,7 @@ While implementing:
- Use named bindings such as `lhs=Expr` to make reduce actions readable.
- Use `%left`, `%right`, or `%precedence` before changing grammar shape solely to fix expression conflicts.
- Use the `error` terminal for recovery points such as delimiters, statements, or object members.
- Include `error` in precedence declarations if recovery productions introduce a shift/reduce conflict.
- Keep generated parser files committed only if the project convention does so.

When debugging:
Expand Down
1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Prefer RustyLR when the parser needs:
- Operator precedence and associativity.
- Location-aware diagnostics.
- Panic-mode error recovery.
- Precedence declarations for recovery conflicts involving the reserved `error` terminal.
- Ambiguous grammar support through GLR.
- Parser-state diagnostics for conflict debugging.

Expand Down
2 changes: 1 addition & 1 deletion rusty_lr_parser/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rusty_lr_parser"
version = "4.2.0"
version = "4.2.1"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "grammar line parser for rusty_lr"
Expand Down
114 changes: 114 additions & 0 deletions rusty_lr_parser/src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2801,6 +2801,11 @@ impl Grammar {
}
}
}
if let Some(level) = self.error_precedence {
if !grammar.add_precedence(TerminalSymbol::Error, level) {
unreachable!("set_reduce_type error");
}
}
grammar.set_precedence_types(self.precedence_types.iter().map(|op| *op.value()).collect());

// add rules
Expand Down Expand Up @@ -3547,6 +3552,115 @@ mod tests {
assert_eq!(grammar_args.rules[0].name.value(), "Expr");
}

#[test]
fn test_parse_rule_line_action_error_recovery() {
let input = quote! {
%tokentype char;
%start Expr;
Expr : 'a' = ;
Term : 'b';
};

let grammar_args =
Grammar::parse_args(input).expect("Should recover from malformed reduce action");

assert_eq!(grammar_args.error_recovered.len(), 1);
assert_eq!(
grammar_args.error_recovered[0].message,
"Expected reduce action block or rule terminator"
);
assert_eq!(grammar_args.rules.len(), 2);
assert_eq!(grammar_args.rules[0].name.value(), "Expr");
assert_eq!(grammar_args.rules[1].name.value(), "Term");
}

#[test]
fn test_parse_mapped_symbol_error_recovery() {
let input = quote! {
%tokentype char;
%start Expr;
Expr : lhs= % 'a';
Term : 'b';
};

let grammar_args =
Grammar::parse_args(input).expect("Should recover from malformed symbol binding");

assert_eq!(grammar_args.error_recovered.len(), 1);
assert_eq!(
grammar_args.error_recovered[0].message,
"Expected pattern after symbol binding"
);
assert_eq!(grammar_args.rules.len(), 2);
assert_eq!(grammar_args.rules[0].name.value(), "Expr");
assert_eq!(grammar_args.rules[1].name.value(), "Term");
}

#[test]
fn test_parse_terminal_set_error_recovery() {
let input = quote! {
%tokentype char;
%start Expr;
Expr : [ % ];
Term : 'b';
};

let grammar_args =
Grammar::parse_args(input).expect("Should recover from malformed terminal set");

assert_eq!(grammar_args.error_recovered.len(), 1);
assert_eq!(
grammar_args.error_recovered[0].message,
"Expected terminal set item"
);
assert_eq!(grammar_args.rules.len(), 2);
assert_eq!(grammar_args.rules[0].name.value(), "Expr");
assert_eq!(grammar_args.rules[1].name.value(), "Term");
}

#[test]
fn test_parse_allow_target_error_recovery() {
let input = quote! {
%tokentype char;
%start Expr;
%allow shift_reduce_conflict_resolved(%);
Expr : 'a';
};

let grammar_args =
Grammar::parse_args(input).expect("Should recover from malformed allow target");

assert_eq!(grammar_args.error_recovered.len(), 1);
assert_eq!(
grammar_args.error_recovered[0].message,
"Expected diagnostic suppression target"
);
assert_eq!(grammar_args.rules.len(), 1);
assert_eq!(grammar_args.rules[0].name.value(), "Expr");
}

#[test]
fn test_parse_rule_body_error_recovery() {
let input = quote! {
%tokentype char;
%start Expr;
Expr : { 1 } = ;
Term : 'b';
};

let grammar_args =
Grammar::parse_args(input).expect("Should recover from malformed rule body");

assert_eq!(grammar_args.error_recovered.len(), 1);
assert_eq!(
grammar_args.error_recovered[0].message,
"Expected semicolon or rule alternative"
);
assert_eq!(grammar_args.rules.len(), 2);
assert_eq!(grammar_args.rules[0].name.value(), "Expr");
assert_eq!(grammar_args.rules[1].name.value(), "Term");
}

#[test]
fn test_bison_variables_success() {
let input = quote! {
Expand Down
65 changes: 62 additions & 3 deletions rusty_lr_parser/src/parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,26 @@ Rule(box RuleDefArgs) : ident RuleType colon RuleLines semicolon {
let $ident = ident else { // "$ident" replaced with "$ident" in the macro expansion
unreachable!( "Rule-Ident" );
};
if let Some(fisrt) = RuleLines.first_mut() {
fisrt.separator_location = @colon;
if let Some(first) = RuleLines.first_mut() {
first.separator_location = @colon;
}
RuleDefArgs {
name: Located::new(ident.to_string(), @ident),
typename: RuleType.map(|t| t.stream()),
rule_lines: RuleLines,
}
}
| ident RuleType colon RuleLines error semicolon {
let $ident = ident else {
unreachable!( "Rule-Ident" );
};
data.error_recovered.push( RecoveredError {
message: "Expected semicolon or rule alternative".to_string(),
link: "https://github.com/ehwan/RustyLR/blob/main/SYNTAX.md#production-rules".to_string(),
location: @error,
});
if let Some(first) = RuleLines.first_mut() {
first.separator_location = @colon;
}
RuleDefArgs {
name: Located::new(ident.to_string(), @ident),
Expand Down Expand Up @@ -176,6 +194,17 @@ MappedSymbol(box (Option<Located<String>>, PatternArgs)): Pattern {
};
( Some(Located::new(ident.to_string(), @ident)), Pattern )
}
| ident equal error {
let $ident = ident else {
unreachable!( "Token-Ident" );
};
data.error_recovered.push( RecoveredError {
message: "Expected pattern after symbol binding".to_string(),
link: "https://github.com/ehwan/RustyLR/blob/main/SYNTAX.md#patterns".to_string(),
location: @error,
});
( Some(Located::new(ident.to_string(), @ident)), PatternArgs::Ident(Default::default()) )
}
;

TerminalSetItem(TerminalSetItem): ident {
Expand Down Expand Up @@ -258,6 +287,19 @@ TerminalSet(TerminalSet): lbracket caret? TerminalSetItem* rbracket {
close_location: @rbracket,
}
}
| lbracket caret? error rbracket {
data.error_recovered.push( RecoveredError {
message: "Expected terminal set item".to_string(),
link: "https://github.com/ehwan/RustyLR/blob/main/SYNTAX.md#patterns".to_string(),
location: @error,
});
TerminalSet {
negate: caret.is_some(),
items: vec![],
open_location: @lbracket,
close_location: @rbracket,
}
}
| dot {
let span = @dot;
TerminalSet {
Expand All @@ -271,6 +313,8 @@ TerminalSet(TerminalSet): lbracket caret? TerminalSetItem* rbracket {

%left minus;
%left star plus question exclamation;
%precedence empty_action;
%precedence error;

Pattern(PatternArgs): ident {
let $ident = ident else {
Expand Down Expand Up @@ -452,7 +496,15 @@ Action(Option<Group>): bracegroup {
};
Some(bracegroup)
}
| { None }
| error {
data.error_recovered.push( RecoveredError {
message: "Expected reduce action block or rule terminator".to_string(),
link: "https://github.com/ehwan/RustyLR/blob/main/SYNTAX.md#reduceaction-optional".to_string(),
location: @error,
});
None
}
| %prec empty_action { None }
;

IdentOrLiteral(IdentOrLiteral): ident {
Expand Down Expand Up @@ -683,6 +735,13 @@ Directive
};
data.allowed_diagnostics.push((Located::new(ident.to_string(), @ident), Some(AllowTarget)));
}
| percent allow ident lparen error rparen semicolon {
data.error_recovered.push( RecoveredError {
message: "Expected diagnostic suppression target".to_string(),
link: "https://github.com/ehwan/RustyLR/blob/main/SYNTAX.md#diagnostic-suppression".to_string(),
location: @error,
});
}
| percent allow error semicolon {
data.error_recovered.push( RecoveredError {
message: "Expected diagnostic name".to_string(),
Expand Down
Loading
Loading