diff --git a/compiler/parse/src/expr.rs b/compiler/parse/src/expr.rs index 9a7e593658..b2c063265d 100644 --- a/compiler/parse/src/expr.rs +++ b/compiler/parse/src/expr.rs @@ -11,9 +11,9 @@ use crate::keyword; use crate::number_literal::number_literal; use crate::parser::{ self, allocated, and_then_with_indent_level, ascii_char, ascii_string, attempt, backtrackable, - fail, map, newline_char, not, not_followed_by, optional, sep_by1, specialize, specialize_ref, - then, unexpected, unexpected_eof, word1, word2, EExpr, ELambda, Either, ParseResult, Parser, - State, SyntaxError, When, + fail, map, newline_char, not, not_followed_by, optional, sep_by1, sep_by1_e, specialize, + specialize_ref, then, unexpected, unexpected_eof, word1, word2, EExpr, ELambda, Either, + ParseResult, Parser, State, SyntaxError, When, }; use crate::pattern::loc_closure_param; use crate::type_annotation; @@ -991,10 +991,10 @@ fn closure_help<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>, ELambda<'a>> { word1(b'\\', ELambda::Start), // Once we see the '\', we're committed to parsing this as a closure. // It may turn out to be malformed, but it is definitely a closure. - optional(and!( + and!( // Parse the params // Params are comma-separated - sep_by1( + sep_by1_e( word1(b',', ELambda::Comma), space0_around_ee( specialize(ELambda::Pattern, loc_closure_param(min_indent)), @@ -1002,7 +1002,8 @@ fn closure_help<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>, ELambda<'a>> { ELambda::Space, ELambda::IndentArg, ELambda::IndentArrow - ) + ), + ELambda::Arg, ), skip_first!( // Parse the -> which separates params from body @@ -1018,16 +1019,13 @@ fn closure_help<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>, ELambda<'a>> { ELambda::IndentBody ) ) - )) + ) ), - |arena: &'a Bump, opt_contents| match opt_contents { - None => Expr::MalformedClosure, - Some((params, loc_body)) => { - let params: Vec<'a, Located>> = params; - let params: &'a [Located>] = params.into_bump_slice(); + |arena: &'a Bump, (params, loc_body)| { + let params: Vec<'a, Located>> = params; + let params: &'a [Located>] = params.into_bump_slice(); - Expr::Closure(params, arena.alloc(loc_body)) - } + Expr::Closure(params, arena.alloc(loc_body)) } ) } @@ -1792,25 +1790,6 @@ fn record_literal<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>, SyntaxError< ) } -/// This is mainly for matching tags in closure params, e.g. \@Foo -> ... -pub fn private_tag<'a>() -> impl Parser<'a, &'a str, SyntaxError<'a>> { - map_with_arena!( - skip_first!(ascii_char(b'@'), global_tag()), - |arena: &'a Bump, name: &'a str| { - let mut buf = String::with_capacity_in(1 + name.len(), arena); - - buf.push('@'); - buf.push_str(name); - buf.into_bump_str() - } - ) -} - -/// This is mainly for matching tags in closure params, e.g. \Foo -> ... -pub fn global_tag<'a>() -> impl Parser<'a, &'a str, SyntaxError<'a>> { - global_tag_or_ident(|first_char| first_char.is_uppercase()) -} - pub fn string_literal<'a>() -> impl Parser<'a, Expr<'a>, SyntaxError<'a>> { map!(crate::string_literal::parse(), Expr::Str) } diff --git a/compiler/parse/src/parser.rs b/compiler/parse/src/parser.rs index 2d1cf0fdb0..e0ea1b2c66 100644 --- a/compiler/parse/src/parser.rs +++ b/compiler/parse/src/parser.rs @@ -392,6 +392,7 @@ pub enum ELambda<'a> { Start(Row, Col), Arrow(Row, Col), Comma(Row, Col), + Arg(Row, Col), // TODO make EEXpr Pattern(EPattern<'a>, Row, Col), Syntax(&'a SyntaxError<'a>, Row, Col), @@ -1204,8 +1205,6 @@ where buf.push(next_output); } Err((element_progress, fail, state)) => { - // If the delimiter parsed, but the following - // element did not, that's a fatal error. return Err((element_progress, fail, state)); } } @@ -1233,6 +1232,84 @@ where } } +/// Parse one or more values separated by a delimiter (e.g. a comma) whose +/// values are discarded +pub fn sep_by1_e<'a, P, V, D, Val, Error>( + delimiter: D, + parser: P, + to_element_error: V, +) -> impl Parser<'a, Vec<'a, Val>, Error> +where + D: Parser<'a, (), Error>, + P: Parser<'a, Val, Error>, + V: Fn(Row, Col) -> Error, + Error: 'a, +{ + move |arena, state: State<'a>| { + let start_bytes_len = state.bytes.len(); + + match parser.parse(arena, state) { + Ok((progress, first_output, next_state)) => { + debug_assert_eq!(progress, MadeProgress); + let mut state = next_state; + let mut buf = Vec::with_capacity_in(1, arena); + + buf.push(first_output); + + loop { + match delimiter.parse(arena, state) { + Ok((_, (), next_state)) => { + // If the delimiter passed, check the element parser. + match parser.parse(arena, next_state) { + Ok((_, next_output, next_state)) => { + state = next_state; + buf.push(next_output); + } + Err((MadeProgress, fail, state)) => { + return Err((MadeProgress, fail, state)); + } + Err((NoProgress, _fail, state)) => { + return Err(( + NoProgress, + to_element_error(state.line, state.column), + state, + )); + } + } + } + Err((delim_progress, fail, old_state)) => { + match delim_progress { + MadeProgress => { + // fail if the delimiter made progress + return Err((MadeProgress, fail, old_state)); + } + NoProgress => { + let progress = Progress::from_lengths( + start_bytes_len, + old_state.bytes.len(), + ); + return Ok((progress, buf, old_state)); + } + } + } + } + } + } + + Err((MadeProgress, fail, state)) => { + return Err((MadeProgress, fail, state)); + } + Err((NoProgress, _fail, state)) => { + return Err(( + NoProgress, + to_element_error(state.line, state.column), + state, + )); + } + } + } +} + pub fn fail_when_progress( progress: Progress, fail: E, diff --git a/compiler/reporting/src/error/parse.rs b/compiler/reporting/src/error/parse.rs index 3431c87379..7745638452 100644 --- a/compiler/reporting/src/error/parse.rs +++ b/compiler/reporting/src/error/parse.rs @@ -173,10 +173,238 @@ fn to_expr_report<'a>( match parse_problem { EExpr::When(when, row, col) => to_when_report(alloc, filename, context, &when, *row, *col), + EExpr::Lambda(lambda, row, col) => { + to_lambda_report(alloc, filename, context, &lambda, *row, *col) + } _ => todo!("unhandled parse error: {:?}", parse_problem), } } +fn to_lambda_report<'a>( + alloc: &'a RocDocAllocator<'a>, + filename: PathBuf, + context: Context, + parse_problem: &roc_parse::parser::ELambda<'a>, + start_row: Row, + start_col: Col, +) -> Report<'a> { + use roc_parse::parser::ELambda; + + match *parse_problem { + ELambda::Arrow(row, col) => match what_is_next(alloc.src_lines, row, col) { + Next::Token("=>") => { + let surroundings = Region::from_rows_cols(start_row, start_col, row, col); + let region = Region::from_row_col(row, col); + + let doc = alloc.stack(vec![ + alloc + .reflow(r"I am in the middle of parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(surroundings, region), + alloc.concat(vec![ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD ARROW".to_string(), + } + } + _ => { + let surroundings = Region::from_rows_cols(start_row, start_col, row, col); + let region = Region::from_row_col(row, col); + + let doc = alloc.stack(vec![ + alloc + .reflow(r"I am in the middle of parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(surroundings, region), + alloc.concat(vec![ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "MISSING ARROW".to_string(), + } + } + }, + + ELambda::Comma(row, col) => match what_is_next(alloc.src_lines, row, col) { + Next::Token("=>") => { + let surroundings = Region::from_rows_cols(start_row, start_col, row, col); + let region = Region::from_row_col(row, col); + + let doc = alloc.stack(vec![ + alloc + .reflow(r"I am in the middle of parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(surroundings, region), + alloc.concat(vec![ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD ARROW".to_string(), + } + } + _ => { + let surroundings = Region::from_rows_cols(start_row, start_col, row, col); + let region = Region::from_row_col(row, col); + + let doc = alloc.stack(vec![ + alloc + .reflow(r"I am in the middle of parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(surroundings, region), + alloc.concat(vec![ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "MISSING ARROW".to_string(), + } + } + }, + + ELambda::Arg(row, col) => match what_is_next(alloc.src_lines, row, col) { + Next::Other(Some(',')) => { + let surroundings = Region::from_rows_cols(start_row, start_col, row, col); + let region = Region::from_row_col(row, col); + + let doc = alloc.stack(vec![ + alloc + .reflow(r"I am in the middle of parsing a function argument list, but I got stuck at this comma:"), + alloc.region_with_subregion(surroundings, region), + alloc.concat(vec![ + alloc.reflow("I was expecting an argument pattern before this, "), + alloc.reflow("so try adding an argument before the comma and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED ARGUMENT LIST".to_string(), + } + } + _ => { + let surroundings = Region::from_rows_cols(start_row, start_col, row, col); + let region = Region::from_row_col(row, col); + + let doc = alloc.stack(vec![ + alloc + .reflow(r"I am in the middle of parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(surroundings, region), + alloc.concat(vec![ + alloc.reflow("I was expecting an argument pattern before this, "), + alloc.reflow("so try adding an argument and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "MISSING ARROW".to_string(), + } + } + }, + + ELambda::Start(_row, _col) => unreachable!("another branch would have been taken"), + + ELambda::Syntax(syntax, row, col) => to_syntax_report(alloc, filename, syntax, row, col), + ELambda::Pattern(ref pattern, row, col) => { + to_pattern_report(alloc, filename, pattern, row, col) + } + ELambda::Space(error, row, col) => to_space_report(alloc, filename, &error, row, col), + + ELambda::IndentArrow(row, col) => to_unfinished_lambda_report( + alloc, + filename, + row, + col, + start_row, + start_col, + alloc.concat(vec![ + alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ), + + ELambda::IndentBody(row, col) => to_unfinished_lambda_report( + alloc, + filename, + row, + col, + start_row, + start_col, + alloc.concat(vec![ + alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ), + + ELambda::IndentArg(row, col) => to_unfinished_lambda_report( + alloc, + filename, + row, + col, + start_row, + start_col, + alloc.concat(vec![ + alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ), + } +} + +fn to_unfinished_lambda_report<'a>( + alloc: &'a RocDocAllocator<'a>, + filename: PathBuf, + row: Row, + col: Col, + start_row: Row, + start_col: Col, + message: RocDocBuilder<'a>, +) -> Report<'a> { + let surroundings = Region::from_rows_cols(start_row, start_col, row, col); + let region = Region::from_row_col(row, col); + + let doc = alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow(r"I was partway through parsing a "), + alloc.reflow(r" function, but I got stuck here:"), + ]), + alloc.region_with_subregion(surroundings, region), + message, + // note_for_when_error(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED FUNCTION".to_string(), + } +} + fn to_when_report<'a>( alloc: &'a RocDocAllocator<'a>, filename: PathBuf, @@ -1635,6 +1863,7 @@ fn what_is_next<'a>(source_lines: &'a [&'a str], row: Row, col: Col) -> Next<'a> ']' => Next::Close("square bracket", ']'), '}' => Next::Close("curly brace", '}'), '-' if it.next() == Some('>') => Next::Token("->"), + '=' if it.next() == Some('>') => Next::Token("=>"), // _ if is_symbol(c) => todo!("it's an operator"), _ => Next::Other(Some(c)), }, diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index 435f092b1d..24686998cf 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -4912,6 +4912,56 @@ mod test_reporting { ) } + #[test] + fn lambda_double_comma() { + report_problem_as( + indoc!( + r#" + \a,,b -> 1 + "# + ), + indoc!( + r#" + ── UNFINISHED ARGUMENT LIST ──────────────────────────────────────────────────── + + I am in the middle of parsing a function argument list, but I got + stuck at this comma: + + 1│ \,b -> 1 + ^ + + I was expecting an argument pattern before this, so try adding an + argument before the comma and see if that helps? + "# + ), + ) + } + + #[test] + fn lambda_leading_comma() { + report_problem_as( + indoc!( + r#" + \,b -> 1 + "# + ), + indoc!( + r#" + ── UNFINISHED ARGUMENT LIST ──────────────────────────────────────────────────── + + I am in the middle of parsing a function argument list, but I got + stuck at this comma: + + 1│ \,b -> 1 + ^ + + I was expecting an argument pattern before this, so try adding an + argument before the comma and see if that helps? + "# + ), + ) + } + #[test] fn when_outdented_branch() { // this should get better with time