Skip to content

Commit

Permalink
Add error recovery
Browse files Browse the repository at this point in the history
Adds a new expression `error!{"message" e}` which reports an error and
attempts to recover it with `e`. Also `error_if!{}` and
`error_unless!{}`.

Maintains and returns a list of errors which have been recovered.

This enables the parser to report multiple errors, which is useful
in contexts like IDEs and compilers where the user may find it easier
to be presented with all errors rather than just the first.
  • Loading branch information
kw217 committed Feb 16, 2022
1 parent 90e9b40 commit bcc4456
Show file tree
Hide file tree
Showing 11 changed files with 1,366 additions and 197 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Helpful `rustc` error messages for errors in the grammar definition or the Rust
code embedded within it
* Rule-level tracing to debug grammars
* Error recovery

## Example

Expand All @@ -39,12 +40,12 @@ pub fn main() {

## Comparison with similar parser generators

| crate | parser type | action code | integration | input type | precedence climbing | parameterized rules | streaming input |
|----------- |------------- |------------- |-------------------- |------------------------ |--------------------- |-------------------- |----------------- |
| peg | PEG | in grammar | proc macro (block) | `&str`, `&[T]`, custom | Yes | Yes | No |
| [pest] | PEG | external | proc macro (file) | `&str` | Yes | No | No |
| [nom] | combinators | in source | library | `&[u8]`, custom | No | Yes | Yes |
| [lalrpop] | LR(1) | in grammar | build script | `&str` | No | Yes | No |
| crate | parser type | action code | integration | input type | precedence climbing | parameterized rules | streaming input | recovery |
|----------- |------------- |------------- |-------------------- |------------------------ |--------------------- |-------------------- |----------------- |--------- |
| peg | PEG | in grammar | proc macro (block) | `&str`, `&[T]`, custom | Yes | Yes | No | Yes |
| [pest] | PEG | external | proc macro (file) | `&str` | Yes | No | No | No |
| [nom] | combinators | in source | library | `&[u8]`, custom | No | Yes | Yes | No |
| [lalrpop] | LR(1) | in grammar | build script | `&str` | No | Yes | No | Yes |

[pest]: https://github.com/pest-parser/pest
[nom]: https://github.com/geal/nom
Expand All @@ -63,7 +64,8 @@ pub fn main() {

## Upgrade guide

The rule return type has changed between 0.8 to 0.9.
The rule return type has changed between 0.8 to 0.9,
and now supports recovery and reporting multiple errors.
To upgrade, add a call to `.into_result()` to convert the new rule return type
to a simple `Result`.
## Development
Expand Down
10 changes: 5 additions & 5 deletions peg-macros/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ impl<'a> LeftRecursionVisitor<'a> {
nullable
}

LiteralExpr(_) | PatternExpr(_) | MethodExpr(_, _) | FailExpr(_) | MarkerExpr(_) => false,
LiteralExpr(_) | PatternExpr(_) | MethodExpr(_, _) | FailExpr(_) | ErrorIfExpr(..) | ErrorUnlessExpr(..) |MarkerExpr(_) => false,

PositionExpr => true,
}
Expand Down Expand Up @@ -220,7 +220,7 @@ impl<'a> LoopNullabilityVisitor<'a> {
let name = rule_ident.to_string();
*self.rule_nullability.get(&name).unwrap_or(&false)
}

ActionExpr(ref elems, ..) => {
let mut nullable = true;
for elem in elems {
Expand Down Expand Up @@ -250,7 +250,7 @@ impl<'a> LoopNullabilityVisitor<'a> {
if inner_nullable && sep_nullable && !bound.has_upper_bound() {
self.errors.push(LoopNullabilityError { span: this_expr.span });
}

inner_nullable | !bound.has_lower_bound()
}

Expand All @@ -269,10 +269,10 @@ impl<'a> LoopNullabilityVisitor<'a> {
}
}

nullable
nullable
}

LiteralExpr(_) | PatternExpr(_) | MethodExpr(_, _) | FailExpr(_) | MarkerExpr(_) => false,
LiteralExpr(_) | PatternExpr(_) | MethodExpr(_, _) | FailExpr(_) | ErrorIfExpr(..) | ErrorUnlessExpr(..) | MarkerExpr(_) => false,
PositionExpr => true,
}
}
Expand Down
2 changes: 2 additions & 0 deletions peg-macros/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ pub enum Expr {
PositionExpr,
QuietExpr(Box<SpannedExpr>),
FailExpr(Literal),
ErrorIfExpr(Box<SpannedExpr>, Literal, Box<SpannedExpr>),
ErrorUnlessExpr(Box<SpannedExpr>, Literal, Box<SpannedExpr>),
PrecedenceExpr {
levels: Vec<PrecedenceLevel>,
},
Expand Down
990 changes: 821 additions & 169 deletions peg-macros/grammar.rs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions peg-macros/grammar.rustpeg
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ rule primary() -> SpannedExpr
/ sp:sp() "position" "!" "(" ")" { PositionExpr.at(sp) }
/ sp:sp() "quiet" "!" "{" e:expression() "}" { QuietExpr(Box::new(e)).at(sp) }
/ sp:sp() "expected" "!" "(" s:LITERAL() ")" { FailExpr(s).at(sp) }
/ sp:sp() "error" "!" "{" s:LITERAL() seq:sequence() "}" { ErrorIfExpr(Box::new(ActionExpr(vec![], None).at(sp)), s, Box::new(seq)).at(sp) }
/ sp:sp() "error_if" "!" "{" seq1:sequence() "|" s:LITERAL() seq2:sequence() "}" { ErrorIfExpr(Box::new(seq1), s, Box::new(seq2)).at(sp) }
/ sp:sp() "error_unless" "!" "{" seq1:sequence() "|" s:LITERAL() seq2:sequence() "}" { ErrorUnlessExpr(Box::new(seq1), s, Box::new(seq2)).at(sp) }
/ &("_" / "__" / "___") sp:sp() name:IDENT() { RuleExpr(name, Vec::new()).at(sp) }
/ sp:sp() name:IDENT() "(" args:(rule_arg() ** ",") ")" { RuleExpr(name, args).at(sp) }
/ sp:sp() l:LITERAL() { LiteralExpr(l).at(sp) }
Expand Down
100 changes: 96 additions & 4 deletions peg-macros/translate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ fn compile_rule(context: &Context, rule: &Rule) -> TokenStream {
::peg::RuleResult::Failed => {
println!("[PEG_TRACE] Failed to match rule `{}` at {}", #str_rule_name, loc);
}
::peg::RuleResult::Error(e) => {
let eloc = ::peg::Parse::position_repr(__input, e.location);
println!("[PEG_TRACE] Error matching rule `{}` at {}: {} at {}", #str_rule_name, loc, e.error, eloc);
}
}

__peg_result
Expand All @@ -249,6 +253,7 @@ fn compile_rule(context: &Context, rule: &Rule) -> TokenStream {
match &entry {
&::peg::RuleResult::Matched(..) => println!("[PEG_TRACE] Cached match of rule {} at {}", #str_rule_name, loc),
&Failed => println!("[PEG_TRACE] Cached fail of rule {} at {}", #str_rule_name, loc),
&::peg::RuleResult::Error(..) => println!("[PEG_TRACE] Cached error of rule {} at {}", #str_rule_name, loc),
};
}
} else {
Expand Down Expand Up @@ -282,10 +287,16 @@ fn compile_rule(context: &Context, rule: &Rule) -> TokenStream {
let __current_result = { #wrapped_body };
match __current_result {
::peg::RuleResult::Failed => break,
::peg::RuleResult::Error(..) => {
__state.#cache_field.insert(__pos, __current_result.clone());
__last_result = __current_result;
break
},
::peg::RuleResult::Matched(__current_endpos, _) =>
match __last_result {
::peg::RuleResult::Matched(__last_endpos, _) if __current_endpos <= __last_endpos => break,
_ => {
::peg::RuleResult::Error(..) => panic!(), // impossible; we would have broken on previous iteration
::peg::RuleResult::Failed | ::peg::RuleResult::Matched(..) => {
__state.#cache_field.insert(__pos, __current_result.clone());
__last_result = __current_result;
},
Expand Down Expand Up @@ -352,7 +363,10 @@ fn compile_rule_export(context: &Context, rule: &Rule) -> TokenStream {
__err_state.mark_failure(__pos, "EOF");
}
}
_ => ()
::peg::RuleResult::Error(__e) => {
return __err_state.into_error(__e, __input)
}
::peg::RuleResult::Failed => ()
}

__state = ParseState::new();
Expand Down Expand Up @@ -387,6 +401,7 @@ fn ordered_choice(span: Span, mut rs: impl DoubleEndedIterator<Item = TokenStrea
let __choice_res = #preferred;
match __choice_res {
::peg::RuleResult::Matched(__pos, __value) => ::peg::RuleResult::Matched(__pos, __value),
::peg::RuleResult::Error(__e) => ::peg::RuleResult::Error(__e),
::peg::RuleResult::Failed => #fallback
}
}}
Expand Down Expand Up @@ -421,6 +436,7 @@ fn compile_expr_continuation(context: &Context, e: &SpannedExpr, result_name: Op
let __seq_res = #seq_res;
match __seq_res {
::peg::RuleResult::Matched(__pos, #result_pat) => { #continuation }
::peg::RuleResult::Error(__e) => ::peg::RuleResult::Error(__e),
::peg::RuleResult::Failed => ::peg::RuleResult::Failed,
}
}}
Expand All @@ -434,6 +450,7 @@ fn compile_literal_expr(s: &Literal, continuation: TokenStream) -> TokenStream {
quote_spanned! { span =>
match ::peg::ParseLiteral::parse_string_literal(__input, __pos, #s) {
::peg::RuleResult::Matched(__pos, __val) => { #continuation }
::peg::RuleResult::Error(__e) => { __err_state.mark_error(__e); ::peg::RuleResult::Error(__e) } // unexpected, but do something sensible
::peg::RuleResult::Failed => { __err_state.mark_failure(__pos, #escaped_str); ::peg::RuleResult::Failed }
}
}
Expand All @@ -457,6 +474,7 @@ fn compile_pattern_expr(pattern_group: &Group, result_name: Ident, success_res:
_ => #not_in_set,
}
::peg::RuleResult::Failed => { __err_state.mark_failure(__pos, #pat_str); ::peg::RuleResult::Failed },
::peg::RuleResult::Error(__e) => { __err_state.mark_error(__e); ::peg::RuleResult::Error(__e) }, // unexpected, but do something sensible
}
}
}
Expand Down Expand Up @@ -543,6 +561,7 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
quote_spanned!{ span=>
match #func(__input, __state, __err_state, __pos #extra_args_call #(, #rule_args_call)*){
::peg::RuleResult::Matched(pos, _) => ::peg::RuleResult::Matched(pos, ()),
::peg::RuleResult::Error(e) => ::peg::RuleResult::Error(e),
::peg::RuleResult::Failed => ::peg::RuleResult::Failed,
}
}
Expand All @@ -567,13 +586,15 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
match #optional_res {
::peg::RuleResult::Matched(__newpos, __value) => { ::peg::RuleResult::Matched(__newpos, Some(__value)) },
::peg::RuleResult::Failed => { ::peg::RuleResult::Matched(__pos, None) },
::peg::RuleResult::Error(__e) => { ::peg::RuleResult::Error(__e) },
}
}
} else {
quote_spanned!{ span=>
match #optional_res {
::peg::RuleResult::Matched(__newpos, _) => { ::peg::RuleResult::Matched(__newpos, ()) },
::peg::RuleResult::Failed => { ::peg::RuleResult::Matched(__pos, ()) },
::peg::RuleResult::Error(__e) => { ::peg::RuleResult::Error(__e) },
}
}
}
Expand All @@ -597,6 +618,7 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
match __sep_res {
::peg::RuleResult::Matched(__newpos, _) => { __newpos },
::peg::RuleResult::Failed => break,
::peg::RuleResult::Error(__e) => { __maybe_err = Some(__e); break }
}
};
}
Expand Down Expand Up @@ -626,7 +648,10 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS

let result_check = if let Some(min) = min {
quote_spanned!{ span=>
if __repeat_value.len() >= #min {
if let Some(__e) = __maybe_err {
::peg::RuleResult::Error(__e)
}
else if __repeat_value.len() >= #min {
::peg::RuleResult::Matched(__repeat_pos, #result)
} else {
::peg::RuleResult::Failed
Expand All @@ -638,6 +663,7 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS

quote_spanned!{ span=> {
let mut __repeat_pos = __pos;
let mut __maybe_err = None;
#repeat_vec

loop {
Expand All @@ -655,6 +681,10 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
::peg::RuleResult::Failed => {
break;
}
::peg::RuleResult::Error(__e) => {
__maybe_err = Some(__e);
break;
}
}
}

Expand All @@ -671,6 +701,7 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
match __assert_res {
::peg::RuleResult::Matched(_, __value) => ::peg::RuleResult::Matched(__pos, __value),
::peg::RuleResult::Failed => ::peg::RuleResult::Failed,
::peg::RuleResult::Error(..) => ::peg::RuleResult::Failed,
}
}}
}
Expand All @@ -683,6 +714,7 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
__err_state.suppress_fail -= 1;
match __assert_res {
::peg::RuleResult::Failed => ::peg::RuleResult::Matched(__pos, ()),
::peg::RuleResult::Error(..) => ::peg::RuleResult::Matched(__pos, ()),
::peg::RuleResult::Matched(..) => __err_state.mark_failure(__pos, "mismatch"),
}
}}
Expand Down Expand Up @@ -717,6 +749,7 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
match #inner {
::peg::RuleResult::Matched(__newpos, _) => { ::peg::RuleResult::Matched(__newpos, ::peg::ParseSlice::parse_slice(__input, str_start, __newpos)) },
::peg::RuleResult::Failed => ::peg::RuleResult::Failed,
::peg::RuleResult::Error(__e) => ::peg::RuleResult::Error(__e),
}
}}
}
Expand All @@ -735,7 +768,66 @@ fn compile_expr(context: &Context, e: &SpannedExpr, result_used: bool) -> TokenS
FailExpr(ref expected) => {
quote_spanned! { span => { __err_state.mark_failure(__pos, #expected); ::peg::RuleResult::Failed }}
}

ErrorIfExpr(ref expr1, ref message, ref expr2) => {
let if_res = compile_expr(context, expr1, false);
let recover_res = compile_expr(context, expr2, result_used);
quote_spanned! { span => {
let __if_res = { #if_res };
match __if_res {
::peg::RuleResult::Failed => ::peg::RuleResult::Failed,
::peg::RuleResult::Error(__e) => ::peg::RuleResult::Error(__e),
::peg::RuleResult::Matched(__newpos, _) => {
// Report error (if any) at start of `expr1`, then consume it by shadowing `__pos`.
let __parse_err = ::peg::error::ParseErr { error: #message, location: __pos };
let __pos = __newpos;
if __err_state.suppress_fail == 0 {
__err_state.suppress_fail += 1;
let __recover_res = { #recover_res };
__err_state.suppress_fail -= 1;

match __recover_res {
::peg::RuleResult::Matched(__newpos, __value) => {
__err_state.mark_error(__parse_err);
::peg::RuleResult::Matched(__newpos, __value)
},
::peg::RuleResult::Failed | ::peg::RuleResult::Error(..) => ::peg::RuleResult::Error(__parse_err)
}
} else {
::peg::RuleResult::Error(__parse_err)
}
},
}
}}
}
ErrorUnlessExpr(ref expr1, ref message, ref expr2) => {
let unless_res = compile_expr(context, expr1, result_used);
let recover_res = compile_expr(context, expr2, result_used);
quote_spanned! { span => {
let __unless_res = { #unless_res };
match __unless_res {
::peg::RuleResult::Matched(__newpos, __value) => ::peg::RuleResult::Matched(__newpos, __value),
::peg::RuleResult::Error(__e) => ::peg::RuleResult::Error(__e),
::peg::RuleResult::Failed => {
let __parse_err = ::peg::error::ParseErr { error: #message, location: __pos };
if __err_state.suppress_fail == 0 {
__err_state.suppress_fail += 1;
let __recover_res = { #recover_res };
__err_state.suppress_fail -= 1;

match __recover_res {
::peg::RuleResult::Matched(__newpos, __value) => {
__err_state.mark_error(__parse_err);
::peg::RuleResult::Matched(__newpos, __value)
},
::peg::RuleResult::Failed | ::peg::RuleResult::Error(..) => ::peg::RuleResult::Error(__parse_err)
}
} else {
::peg::RuleResult::Error(__parse_err)
}
},
}
}}
}
PrecedenceExpr { ref levels } => {
let mut pre_rules = Vec::new();
let mut level_code = Vec::new();
Expand Down
Loading

0 comments on commit bcc4456

Please sign in to comment.