Skip to content

Commit

Permalink
fix: Display what tokens are expected when the parser fails
Browse files Browse the repository at this point in the history
Fixes #270
  • Loading branch information
Marwes committed May 6, 2017
1 parent d421174 commit 6925c7b
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 16 deletions.
7 changes: 6 additions & 1 deletion parser/src/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ AtomicKind: ArcKind = {
"Type" => Ok(Kind::typ()),
"Row" => Ok(Kind::row()),
id => Err(ParseError::User {
error: pos::spanned2(l.into(), r.into(), Error::UnexpectedToken("Identifier".to_string())),
error: pos::spanned2(
l.into(),
r.into(),
Error::UnexpectedToken(
"Identifier".to_string(),
["_", "Row", "Type"].iter().map(|s| s.to_string()).collect())),
}),
}
},
Expand Down
58 changes: 50 additions & 8 deletions parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extern crate gluon_base as base;
extern crate lalrpop_util;

use std::cell::RefCell;
use std::fmt;

use base::ast::{Expr, IdentEnv, SpannedExpr, SpannedPattern, TypedIdent};
use base::error::Errors;
Expand Down Expand Up @@ -80,6 +81,27 @@ fn transform_errors<'a, Iter>(errors: Iter) -> Errors<Spanned<Error, BytePos>>
errors.into_iter().map(Error::from_lalrpop).collect()
}

struct Expected<'a>(&'a [String]);

impl<'a> fmt::Display for Expected<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0.len() {
0 => (),
1 => write!(f, "\nExpected ")?,
_ => write!(f, "\nExpected one of ")?,
}
for (i, token) in self.0.iter().enumerate() {
let sep = match i {
0 => "",
i if i + 1 < self.0.len() => ",",
_ => " or",
};
write!(f, "{} {}", sep, token)?;
}
Ok(())
}
}

quick_error! {
#[derive(Debug, PartialEq)]
pub enum Error {
Expand All @@ -97,13 +119,13 @@ quick_error! {
description("invalid token")
display("Invalid token")
}
UnexpectedToken(token: String) {
UnexpectedToken(token: String, expected: Vec<String>) {
description("unexpected token")
display("Unexpected token: {}", token)
display("Unexpected token: {}{}", token, Expected(&expected))
}
UnexpectedEof {
UnexpectedEof(expected: Vec<String>) {
description("unexpected end of file")
display("Unexpected end of file")
display("Unexpected end of file{}", Expected(&expected))
}
ExtraToken(token: String) {
description("extra token")
Expand All @@ -117,17 +139,37 @@ quick_error! {
}
}

/// LALRPOP currently has an unnecessary set of `"` around each expected token
fn remove_extra_quotes(tokens: &mut [String]) {
for token in tokens {
if token.starts_with('"') && token.ends_with('"') {
token.remove(0);
token.pop();
}
}
}

impl Error {
fn from_lalrpop(err: LalrpopError) -> Spanned<Error, BytePos> {
use lalrpop_util::ParseError::*;

match err {
InvalidToken { location } => pos::spanned2(location, location, Error::InvalidToken),
UnrecognizedToken { token: Some((lpos, token, rpos)), .. } => {
pos::spanned2(lpos, rpos, Error::UnexpectedToken(token.to_string()))
UnrecognizedToken {
token: Some((lpos, token, rpos)),
mut expected,
} => {
remove_extra_quotes(&mut expected);
pos::spanned2(lpos,
rpos,
Error::UnexpectedToken(token.to_string(), expected))
}
UnrecognizedToken { token: None, .. } => {
pos::spanned2(0.into(), 0.into(), Error::UnexpectedEof)
UnrecognizedToken {
token: None,
mut expected,
} => {
remove_extra_quotes(&mut expected);
pos::spanned2(0.into(), 0.into(), Error::UnexpectedEof(expected))
}
ExtraToken { token: (lpos, token, rpos) } => {
pos::spanned2(lpos, rpos, Error::ExtraToken(token.to_string()))
Expand Down
29 changes: 22 additions & 7 deletions parser/tests/error_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,25 @@ extern crate gluon_parser as parser;
mod support;

use base::ast::{TypedIdent, Pattern};
use base::pos::{self, BytePos};
use base::pos::{self, BytePos, Spanned};

use parser::{Error, ParseErrors, TokenizeError};

use support::*;

// The expected tokens aren't very interesting since they may change fairly often
fn remove_expected(errors: ParseErrors) -> ParseErrors {
let f = |mut err: Spanned<Error, _>| {
match err.value {
Error::UnexpectedToken(_, ref mut expected) |
Error::UnexpectedEof(ref mut expected) => expected.clear(),
_ => (),
}
err
};
ParseErrors::from(errors.into_iter().map(f).collect::<Vec<_>>())
}

#[test]
fn empty_input() {
let _ = ::env_logger::init();
Expand All @@ -21,9 +34,10 @@ fn empty_input() {
let (expr, err) = result.unwrap_err();
assert_eq!(expr, Some(error()));

let error = Error::UnexpectedEof;
let error = Error::UnexpectedEof(vec![]);
let span = pos::span(BytePos::from(0), BytePos::from(0));
assert_eq!(err, ParseErrors::from(vec![pos::spanned(span, error)]));
assert_eq!(remove_expected(err),
ParseErrors::from(vec![pos::spanned(span, error)]));
}

#[test]
Expand All @@ -41,9 +55,10 @@ fn missing_match_expr() {
Some(case(error(),
vec![(Pattern::Ident(TypedIdent::new(intern("x"))), id("x"))])));

let error = Error::UnexpectedToken("With".into());
let error = Error::UnexpectedToken("With".into(), vec![]);
let span = pos::span(BytePos::from(11), BytePos::from(15));
assert_eq!(err, ParseErrors::from(vec![pos::spanned(span, error)]));
assert_eq!(remove_expected(err),
ParseErrors::from(vec![pos::spanned(span, error)]));
}

#[test]
Expand All @@ -57,11 +72,11 @@ let y =
2
y
"#);
let error = Error::UnexpectedToken("IntLiteral".into());
let error = Error::UnexpectedToken("IntLiteral".into(), vec![]);
let span = pos::span(BytePos::from(32), BytePos::from(32));
let errors = ParseErrors::from(vec![pos::spanned(span, error)]);

assert_eq!(result.map_err(|(_, err)| err), Err(errors));
assert_eq!(result.map_err(|(_, err)| remove_expected(err)), Err(errors));
}

#[test]
Expand Down

0 comments on commit 6925c7b

Please sign in to comment.