diff --git a/compiler/qsc_parse/src/expr.rs b/compiler/qsc_parse/src/expr.rs index c191508a66..c4221fc847 100644 --- a/compiler/qsc_parse/src/expr.rs +++ b/compiler/qsc_parse/src/expr.rs @@ -166,7 +166,22 @@ fn expr_base(s: &mut ParserContext) -> Result> { } else if token(s, TokenKind::Keyword(Keyword::Fail)).is_ok() { Ok(Box::new(ExprKind::Fail(expr(s)?))) } else if token(s, TokenKind::Keyword(Keyword::For)).is_ok() { - let vars = pat(s)?; + let peeked = s.peek(); + let vars = match pat(s) { + Ok(o) => o, + Err(e) => { + if let ( + TokenKind::Open(Delim::Paren), + ErrorKind::Token(_, TokenKind::Keyword(Keyword::In), _), + ) = (peeked.kind, &e.0) + { + return Err( + e.with_help("parenthesis are not permitted around for-loop iterations") + ); + } + return Err(e); + } + }; token(s, TokenKind::Keyword(Keyword::In))?; let iter = expr(s)?; let body = stmt::parse_block(s)?; @@ -209,7 +224,7 @@ fn expr_base(s: &mut ParserContext) -> Result> { } else if let Some(p) = opt(s, single_ident_path)? { Ok(Box::new(ExprKind::Path(p))) } else { - Err(Error(ErrorKind::Rule( + Err(Error::new(ErrorKind::Rule( "expression", s.peek().kind, s.peek().span, @@ -294,7 +309,7 @@ fn expr_set(s: &mut ParserContext) -> Result> { let rhs = expr(s)?; Ok(Box::new(ExprKind::AssignOp(closed_bin_op(op), lhs, rhs))) } else { - Err(Error(ErrorKind::Rule( + Err(Error::new(ErrorKind::Rule( "assignment operator", s.peek().kind, s.peek().span, @@ -355,7 +370,7 @@ fn expr_interpolate(s: &mut ParserContext) -> Result> { let TokenKind::String(StringToken::Interpolated(InterpolatedStart::DollarQuote, mut end)) = token.kind else { - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "interpolated string", token.kind, token.span, @@ -376,7 +391,7 @@ fn expr_interpolate(s: &mut ParserContext) -> Result> { let TokenKind::String(StringToken::Interpolated(InterpolatedStart::RBrace, next_end)) = token.kind else { - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "interpolated string", token.kind, token.span, @@ -419,20 +434,20 @@ fn lit_token(lexeme: &str, token: Token) -> Result> { let offset = if radix == Radix::Decimal { 0 } else { 2 }; let lexeme = &lexeme[offset..lexeme.len() - 1]; // Slice off prefix and suffix. let value = BigInt::from_str_radix(lexeme, radix.into()) - .map_err(|_| Error(ErrorKind::Lit("big-integer", token.span)))?; + .map_err(|_| Error::new(ErrorKind::Lit("big-integer", token.span)))?; Ok(Some(Lit::BigInt(Box::new(value)))) } TokenKind::Float => { let lexeme = lexeme.replace('_', ""); let value = lexeme .parse() - .map_err(|_| Error(ErrorKind::Lit("floating-point", token.span)))?; + .map_err(|_| Error::new(ErrorKind::Lit("floating-point", token.span)))?; Ok(Some(Lit::Double(value))) } TokenKind::Int(radix) => { let offset = if radix == Radix::Decimal { 0 } else { 2 }; let value = lit_int(&lexeme[offset..], radix.into()) - .ok_or(Error(ErrorKind::Lit("integer", token.span)))?; + .ok_or(Error::new(ErrorKind::Lit("integer", token.span)))?; Ok(Some(Lit::Int(value))) } TokenKind::String(StringToken::Normal) => { @@ -445,7 +460,7 @@ fn lit_token(lexeme: &str, token: Token) -> Result> { let index: u32 = index.try_into().expect("index should fit into u32"); let lo = token.span.lo + index + 2; let span = Span { lo, hi: lo + 1 }; - Error(ErrorKind::Escape(ch, span)) + Error::new(ErrorKind::Escape(ch, span)) })?; Ok(Some(Lit::String(string.into()))) } @@ -759,7 +774,7 @@ fn expr_as_pat(expr: Expr) -> Result> { .collect::>()?; Ok(PatKind::Tuple(pats)) } - _ => Err(Error(ErrorKind::Convert( + _ => Err(Error::new(ErrorKind::Convert( "pattern", "expression", expr.span, diff --git a/compiler/qsc_parse/src/expr/tests.rs b/compiler/qsc_parse/src/expr/tests.rs index 0cbf8ce98f..4a64bf8ae6 100644 --- a/compiler/qsc_parse/src/expr/tests.rs +++ b/compiler/qsc_parse/src/expr/tests.rs @@ -2521,3 +2521,83 @@ fn invalid_initial_commas_in_pattern() { ]"#]], ); } + +#[test] +fn friendly_error_on_parenthesized_for() { + check( + expr, + "for (x in xs) { () }", + &expect![[r#" + Error( + Token( + Close( + Paren, + ), + Keyword( + In, + ), + Span { + lo: 7, + hi: 9, + }, + ), + Some( + "parenthesis are not permitted around for-loop iterations", + ), + ) + "#]], + ); +} + +#[test] +fn no_help_text_on_non_parenthesized_for() { + check( + expr, + "for x in invalid syntax { () }", + &expect![[r#" + Error( + Token( + Open( + Brace, + ), + Ident, + Span { + lo: 17, + hi: 23, + }, + ), + ) + "#]], + ); +} + +#[test] +fn parenthesized_pat_in_for_is_valid() { + check( + expr, + "for (x) in xs { () }", + &expect![[r#" + Expr _id_ [0-20]: For: + Pat _id_ [4-7]: Paren: + Pat _id_ [5-6]: Bind: + Ident _id_ [5-6] "x" + Expr _id_ [11-13]: Path: Path _id_ [11-13] (Ident _id_ [11-13] "xs") + Block _id_ [14-20]: + Stmt _id_ [16-18]: Expr: Expr _id_ [16-18]: Unit"#]], + ); +} + +#[test] +fn parenthesized_expr_in_for_is_valid() { + check( + expr, + "for x in (xs) { () }", + &expect![[r#" + Expr _id_ [0-20]: For: + Pat _id_ [4-5]: Bind: + Ident _id_ [4-5] "x" + Expr _id_ [9-13]: Paren: Expr _id_ [10-12]: Path: Path _id_ [10-12] (Ident _id_ [10-12] "xs") + Block _id_ [14-20]: + Stmt _id_ [16-18]: Expr: Expr _id_ [16-18]: Unit"#]], + ); +} diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs index 3633d40b08..a4e75711d7 100644 --- a/compiler/qsc_parse/src/item.rs +++ b/compiler/qsc_parse/src/item.rs @@ -51,19 +51,19 @@ pub(super) fn parse(s: &mut ParserContext) -> Result> { Box::new(ItemKind::ImportOrExport(decl)) } else if visibility.is_some() { let err_item = default(s.span(lo)); - s.push_error(Error(ErrorKind::FloatingVisibility(err_item.span))); + s.push_error(Error::new(ErrorKind::FloatingVisibility(err_item.span))); return Ok(err_item); } else if !attrs.is_empty() { let err_item = default(s.span(lo)); - s.push_error(Error(ErrorKind::FloatingAttr(err_item.span))); + s.push_error(Error::new(ErrorKind::FloatingAttr(err_item.span))); return Ok(err_item); } else if doc.is_some() { let err_item = default(s.span(lo)); - s.push_error(Error(ErrorKind::FloatingDocComment(err_item.span))); + s.push_error(Error::new(ErrorKind::FloatingDocComment(err_item.span))); return Ok(err_item); } else { let p = s.peek(); - return Err(Error(ErrorKind::Rule("item", p.kind, p.span))); + return Err(Error::new(ErrorKind::Rule("item", p.kind, p.span))); }; Ok(Box::new(Item { @@ -133,7 +133,7 @@ fn parse_top_level_node(s: &mut ParserContext) -> Result { if let StmtKind::Item(item) = &mut *stmt.kind { item.doc = doc.into(); } else if !doc.is_empty() { - return Err(Error(ErrorKind::Rule("item", kind, span))); + return Err(Error::new(ErrorKind::Rule("item", kind, span))); } Ok(TopLevelNode::Stmt(stmt)) } @@ -153,7 +153,10 @@ pub fn parse_implicit_namespace(source_name: &str, s: &mut ParserContext) -> Res let items = parse_namespace_block_contents(s)?; recovering_token(s, TokenKind::Eof); if items.is_empty() || s.peek().kind != TokenKind::Eof { - return Err(Error(ErrorKind::ExpectedItem(s.peek().kind, s.span(lo)))); + return Err(Error::new(ErrorKind::ExpectedItem( + s.peek().kind, + s.span(lo), + ))); } let span = s.span(lo); let namespace_name = source_name_to_namespace_name(source_name, span)?; @@ -177,7 +180,7 @@ fn source_name_to_namespace_name(raw: &str, span: Span) -> Result { match component { std::path::Component::Normal(name) => { // strip the extension off, if there is one - let mut name = name.to_str().ok_or(Error(ErrorKind::InvalidFileName( + let mut name = name.to_str().ok_or(Error::new(ErrorKind::InvalidFileName( span, name.to_string_lossy().to_string(), )))?; @@ -192,7 +195,12 @@ fn source_name_to_namespace_name(raw: &str, span: Span) -> Result { namespace.push(ident); } - _ => return Err(Error(ErrorKind::InvalidFileName(span, raw.to_string()))), + _ => { + return Err(Error::new(ErrorKind::InvalidFileName( + span, + raw.to_string(), + ))) + } } } @@ -216,9 +224,9 @@ fn validate_namespace_name(error_span: Span, name: &str) -> Result { // we just directly use the ident parser here instead of trying to recreate // validation rules let ident = ident(&mut s) - .map_err(|_| Error(ErrorKind::InvalidFileName(error_span, name.to_string())))?; + .map_err(|_| Error::new(ErrorKind::InvalidFileName(error_span, name.to_string())))?; if s.peek().kind != TokenKind::Eof { - return Err(Error(ErrorKind::InvalidFileName( + return Err(Error::new(ErrorKind::InvalidFileName( error_span, name.to_string(), ))); @@ -311,7 +319,7 @@ fn parse_open(s: &mut ParserContext) -> Result> { // Peek to see if the next token is a dot -- this means it is likely a dot ident alias, and // we want to provide a more helpful error message if s.peek().kind == TokenKind::Dot { - return Err(Error(ErrorKind::DotIdentAlias(s.peek().span))); + return Err(Error::new(ErrorKind::DotIdentAlias(s.peek().span))); } token(s, TokenKind::Semi)?; @@ -410,7 +418,11 @@ fn parse_ty_def(s: &mut ParserContext) -> Result> { fn ty_as_ident(ty: Ty) -> Result> { let TyKind::Path(path) = *ty.kind else { - return Err(Error(ErrorKind::Convert("identifier", "type", ty.span))); + return Err(Error::new(ErrorKind::Convert( + "identifier", + "type", + ty.span, + ))); }; if let Path { segments: None, @@ -420,7 +432,11 @@ fn ty_as_ident(ty: Ty) -> Result> { { Ok(name) } else { - Err(Error(ErrorKind::Convert("identifier", "type", ty.span))) + Err(Error::new(ErrorKind::Convert( + "identifier", + "type", + ty.span, + ))) } } @@ -433,7 +449,7 @@ fn parse_callable_decl(s: &mut ParserContext) -> Result> { CallableKind::Operation } else { let token = s.peek(); - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "callable declaration", token.kind, token.span, @@ -510,7 +526,7 @@ fn parse_spec_decl(s: &mut ParserContext) -> Result> { Spec::Ctl } } else { - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "specialization", s.peek().kind, s.peek().span, @@ -544,7 +560,7 @@ fn parse_spec_gen(s: &mut ParserContext) -> Result { } else if token(s, TokenKind::Keyword(Keyword::Slf)).is_ok() { Ok(SpecGen::Slf) } else { - Err(Error(ErrorKind::Rule( + Err(Error::new(ErrorKind::Rule( "specialization generator", s.peek().kind, s.peek().span, @@ -557,7 +573,7 @@ pub(super) fn check_input_parens(inputs: &Pat) -> Result<()> { if matches!(*inputs.kind, PatKind::Paren(_) | PatKind::Tuple(_)) { Ok(()) } else { - Err(Error(ErrorKind::MissingParens(inputs.span))) + Err(Error::new(ErrorKind::MissingParens(inputs.span))) } } @@ -578,7 +594,7 @@ fn parse_import_or_export(s: &mut ParserContext) -> Result { TokenKind::Keyword(Keyword::Export) => true, TokenKind::Keyword(Keyword::Import) => false, _ => { - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "import or export", s.peek().kind, s.peek().span, diff --git a/compiler/qsc_parse/src/lib.rs b/compiler/qsc_parse/src/lib.rs index 32e8f40583..5ab2441498 100644 --- a/compiler/qsc_parse/src/lib.rs +++ b/compiler/qsc_parse/src/lib.rs @@ -27,15 +27,77 @@ use std::rc::Rc; use std::result; use thiserror::Error; -#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] -#[error(transparent)] -#[diagnostic(transparent)] -pub struct Error(ErrorKind); +#[derive(Clone, Eq, Error, PartialEq)] +pub struct Error(ErrorKind, Option); + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ErrorKind::fmt(&self.0, f) + } +} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut formatter = f.debug_tuple("Error"); + if self.1.is_some() { + formatter.field(&self.0).field(&self.1) + } else { + formatter.field(&self.0) + } + .finish() + } +} + +impl Diagnostic for Error { + fn code<'a>(&'a self) -> Option> { + self.0.code() + } + + fn severity(&self) -> Option { + self.0.severity() + } + + fn help<'a>(&'a self) -> Option> { + self.1 + .clone() + .map(|help| Box::new(help) as Box) + } + + fn url<'a>(&'a self) -> Option> { + self.0.url() + } + + fn source_code(&self) -> Option<&dyn miette::SourceCode> { + self.0.source_code() + } + + fn labels(&self) -> Option + '_>> { + self.0.labels() + } + + fn related<'a>(&'a self) -> Option + 'a>> { + self.0.related() + } + + fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { + self.0.diagnostic_source() + } +} impl Error { #[must_use] pub fn with_offset(self, offset: u32) -> Self { - Self(self.0.with_offset(offset)) + Self(self.0.with_offset(offset), self.1) + } + + #[must_use] + pub(crate) fn new(kind: ErrorKind) -> Self { + Self(kind, None) + } + + #[must_use] + pub fn with_help(self, help_text: impl Into) -> Self { + Self(self.0, Some(help_text.into())) } } diff --git a/compiler/qsc_parse/src/prim.rs b/compiler/qsc_parse/src/prim.rs index a83dafe4ac..6b0926e48d 100644 --- a/compiler/qsc_parse/src/prim.rs +++ b/compiler/qsc_parse/src/prim.rs @@ -39,7 +39,11 @@ pub(super) fn token(s: &mut ParserContext, t: TokenKind) -> Result<()> { s.advance(); Ok(()) } else { - Err(Error(ErrorKind::Token(t, s.peek().kind, s.peek().span))) + Err(Error::new(ErrorKind::Token( + t, + s.peek().kind, + s.peek().span, + ))) } } @@ -54,7 +58,7 @@ pub(super) fn apos_ident(s: &mut ParserContext) -> Result> { name, })) } else { - Err(Error(ErrorKind::Rule( + Err(Error::new(ErrorKind::Rule( "generic parameter", peek.kind, peek.span, @@ -73,7 +77,11 @@ pub(super) fn ident(s: &mut ParserContext) -> Result> { name, })) } else { - Err(Error(ErrorKind::Rule("identifier", peek.kind, peek.span))) + Err(Error::new(ErrorKind::Rule( + "identifier", + peek.kind, + peek.span, + ))) } } @@ -187,7 +195,7 @@ where while s.peek().kind == TokenKind::Comma { let mut span = s.peek().span; span.hi = span.lo; - s.push_error(Error(ErrorKind::MissingSeqEntry(span))); + s.push_error(Error::new(ErrorKind::MissingSeqEntry(span))); xs.push(T::default().with_span(span)); s.advance(); } @@ -197,7 +205,7 @@ where while s.peek().kind == TokenKind::Comma { let mut span = s.peek().span; span.hi = span.lo; - s.push_error(Error(ErrorKind::MissingSeqEntry(span))); + s.push_error(Error::new(ErrorKind::MissingSeqEntry(span))); xs.push(T::default().with_span(span)); s.advance(); } @@ -262,7 +270,7 @@ fn advanced(s: &ParserContext, from: u32) -> bool { } fn map_rule_name(name: &'static str, error: Error) -> Error { - Error(match error.0 { + Error::new(match error.0 { ErrorKind::Rule(_, found, span) => ErrorKind::Rule(name, found, span), ErrorKind::Convert(_, found, span) => ErrorKind::Convert(name, found, span), kind => kind, diff --git a/compiler/qsc_parse/src/prim/tests.rs b/compiler/qsc_parse/src/prim/tests.rs index 6b6209edcb..d11d034ccf 100644 --- a/compiler/qsc_parse/src/prim/tests.rs +++ b/compiler/qsc_parse/src/prim/tests.rs @@ -63,7 +63,7 @@ fn ident_keyword() { .expect("keyword length should fit into u32"), }; - let expected = Error(match keyword { + let expected = Error::new(match keyword { Keyword::And => { ErrorKind::Rule("identifier", TokenKind::ClosedBinOp(ClosedBinOp::And), span) } diff --git a/compiler/qsc_parse/src/scan.rs b/compiler/qsc_parse/src/scan.rs index c6db2ec40b..628a35a6de 100644 --- a/compiler/qsc_parse/src/scan.rs +++ b/compiler/qsc_parse/src/scan.rs @@ -95,7 +95,7 @@ impl<'a> Scanner<'a> { barriers: Vec::new(), errors: errors .into_iter() - .map(|e| Error(ErrorKind::Lex(e))) + .map(|e| Error::new(ErrorKind::Lex(e))) .collect(), recovered_eof: false, peek: peek.unwrap_or_else(|| eof(input.len())), @@ -123,7 +123,7 @@ impl<'a> Scanner<'a> { self.offset = self.peek.span.hi; let (peek, errors) = next_ok(&mut self.tokens); self.errors - .extend(errors.into_iter().map(|e| Error(ErrorKind::Lex(e)))); + .extend(errors.into_iter().map(|e| Error::new(ErrorKind::Lex(e)))); self.peek = peek.unwrap_or_else(|| eof(self.input.len())); } } diff --git a/compiler/qsc_parse/src/stmt.rs b/compiler/qsc_parse/src/stmt.rs index 9518a8e9f9..5b11f70220 100644 --- a/compiler/qsc_parse/src/stmt.rs +++ b/compiler/qsc_parse/src/stmt.rs @@ -82,7 +82,7 @@ fn parse_local(s: &mut ParserContext) -> Result> { Mutability::Mutable } else { let token = s.peek(); - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "variable binding", token.kind, token.span, @@ -102,7 +102,7 @@ fn parse_qubit(s: &mut ParserContext) -> Result> { } else if token(s, TokenKind::Keyword(Keyword::Borrow)).is_ok() { QubitSource::Dirty } else { - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "qubit binding", s.peek().kind, s.peek().span, @@ -129,7 +129,7 @@ fn parse_qubit_init(s: &mut ParserContext) -> Result> { let lo = s.peek().span.lo; let kind = if let Ok(name) = ident(s) { if name.name.as_ref() != "Qubit" { - return Err(Error(ErrorKind::Convert( + return Err(Error::new(ErrorKind::Convert( "qubit initializer", "identifier", name.span, @@ -143,7 +143,7 @@ fn parse_qubit_init(s: &mut ParserContext) -> Result> { QubitInitKind::Array(size) } else { let token = s.peek(); - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "qubit suffix", token.kind, token.span, @@ -155,7 +155,7 @@ fn parse_qubit_init(s: &mut ParserContext) -> Result> { final_sep.reify(inits, QubitInitKind::Paren, QubitInitKind::Tuple) } else { let token = s.peek(); - return Err(Error(ErrorKind::Rule( + return Err(Error::new(ErrorKind::Rule( "qubit initializer", token.kind, token.span, @@ -177,7 +177,7 @@ pub(super) fn check_semis(s: &mut ParserContext, stmts: &[Box]) { lo: stmt.span.hi, hi: stmt.span.hi, }; - s.push_error(Error(ErrorKind::MissingSemi(span))); + s.push_error(Error::new(ErrorKind::MissingSemi(span))); } } } diff --git a/compiler/qsc_parse/src/ty.rs b/compiler/qsc_parse/src/ty.rs index 3cea5a936b..b498a058ee 100644 --- a/compiler/qsc_parse/src/ty.rs +++ b/compiler/qsc_parse/src/ty.rs @@ -74,7 +74,7 @@ fn arrow(s: &mut ParserContext) -> Result { } else if token(s, TokenKind::FatArrow).is_ok() { Ok(CallableKind::Operation) } else { - Err(Error(ErrorKind::Rule( + Err(Error::new(ErrorKind::Rule( "arrow type", s.peek().kind, s.peek().span, @@ -96,7 +96,11 @@ fn base(s: &mut ParserContext) -> Result { token(s, TokenKind::Close(Delim::Paren))?; Ok(final_sep.reify(tys, |t| TyKind::Paren(Box::new(t)), TyKind::Tuple)) } else { - Err(Error(ErrorKind::Rule("type", s.peek().kind, s.peek().span))) + Err(Error::new(ErrorKind::Rule( + "type", + s.peek().kind, + s.peek().span, + ))) }?; Ok(Ty { @@ -124,7 +128,7 @@ fn functor_base(s: &mut ParserContext) -> Result { } else if token(s, TokenKind::Keyword(Keyword::Ctl)).is_ok() { Ok(FunctorExprKind::Lit(Functor::Ctl)) } else { - Err(Error(ErrorKind::Rule( + Err(Error::new(ErrorKind::Rule( "functor literal", s.peek().kind, s.peek().span,