diff --git a/compiler/parse/src/blankspace.rs b/compiler/parse/src/blankspace.rs index 9b422a9df6..d3f0b70f55 100644 --- a/compiler/parse/src/blankspace.rs +++ b/compiler/parse/src/blankspace.rs @@ -444,6 +444,9 @@ where let mut state = state; let mut any_newlines = false; + let start_row = original_state.line; + let start_col = original_state.column; + let start_bytes_len = state.bytes.len(); while !state.bytes.is_empty() { @@ -491,7 +494,13 @@ where let progress = Progress::from_lengths(start_bytes_len, state.bytes.len()); state = state - .check_indent_e(arena, min_indent, indent_problem) + .check_indent_e( + arena, + min_indent, + indent_problem, + start_row, + start_col, + ) .map_err(|(fail, _)| { (progress, fail, original_state.clone()) })? @@ -523,7 +532,13 @@ where ); if any_newlines { state = state - .check_indent_e(arena, min_indent, indent_problem) + .check_indent_e( + arena, + min_indent, + indent_problem, + start_row, + start_col, + ) .map_err(|(fail, _)| { (progress, fail, original_state.clone()) })?; @@ -692,7 +707,13 @@ where progress, space_slice, state - .check_indent_e(arena, min_indent, indent_problem) + .check_indent_e( + arena, + min_indent, + indent_problem, + start_row, + start_col, + ) .map_err(|(fail, _)| (progress, fail, original_state))?, )); } @@ -720,7 +741,7 @@ where let progress = Progress::from_lengths(start_bytes_len, state.bytes.len()); if any_newlines { state = state - .check_indent_e(arena, min_indent, indent_problem) + .check_indent_e(arena, min_indent, indent_problem, start_row, start_col) .map_err(|(fail, _)| (progress, fail, original_state))?; } diff --git a/compiler/parse/src/parser.rs b/compiler/parse/src/parser.rs index 042ded0dab..653936244c 100644 --- a/compiler/parse/src/parser.rs +++ b/compiler/parse/src/parser.rs @@ -72,12 +72,14 @@ impl<'a> State<'a> { _arena: &'a Bump, min_indent: u16, to_error: TE, + row: Row, + col: Col, ) -> Result where TE: Fn(Row, Col) -> E, { if self.indent_col < min_indent { - Err((to_error(self.line, self.column), self)) + Err((to_error(row, col), self)) } else { Ok(self) } @@ -378,6 +380,7 @@ pub enum Type<'a> { TStart(Row, Col), TEnd(Row, Col), TSpace(BadInputError, Row, Col), + TFunctionArgument(Row, Col), /// TIndentStart(Row, Col), TIndentEnd(Row, Col), @@ -1496,6 +1499,31 @@ where } } +pub fn check_indent<'a, TE, E>(min_indent: u16, to_problem: TE) -> impl Parser<'a, (), E> +where + TE: Fn(Row, Col) -> E, + E: 'a, +{ + move |_arena, state: State<'a>| { + dbg!(state.indent_col, min_indent); + if state.indent_col < min_indent { + Err((NoProgress, to_problem(state.line, state.column), state)) + } else { + Ok((NoProgress, (), state)) + } + } +} + +#[macro_export] +macro_rules! word1_check_indent { + ($word:expr, $word_problem:expr, $min_indent:expr, $indent_problem:expr) => { + and!( + word1($word, $word_problem), + crate::parser::check_indent($min_indent, $indent_problem) + ) + }; +} + #[allow(dead_code)] fn in_context<'a, AddContext, P1, P2, Start, A, X, Y>( add_context: AddContext, diff --git a/compiler/parse/src/type_annotation.rs b/compiler/parse/src/type_annotation.rs index 7e154d9741..6f30227168 100644 --- a/compiler/parse/src/type_annotation.rs +++ b/compiler/parse/src/type_annotation.rs @@ -1,15 +1,13 @@ use crate::ast::{AssignedField, CommentOrNewline, Tag, TypeAnnotation}; -use crate::blankspace::{ - space0, space0_around, space0_around_e, space0_before, space0_before_e, space0_e, -}; +use crate::blankspace::{space0_around_e, space0_before_e, space0_e}; use crate::expr::{global_tag, private_tag}; use crate::ident::join_module_parts; use crate::keyword; use crate::parser::{ - allocated, ascii_char, ascii_string, backtrackable, not_e, optional, peek_utf8_char_e, - specialize, specialize_ref, word1, word2, BadInputError, Col, Either, ParseResult, Parser, + allocated, backtrackable, not_e, optional, peek_utf8_char_e, specialize, specialize_ref, word1, + word2, BadInputError, Either, ParseResult, Parser, Progress::{self, *}, - Row, State, SyntaxError, TApply, TInParens, TRecord, TTagUnion, TVariable, Type, + State, SyntaxError, TApply, TInParens, TRecord, TTagUnion, TVariable, Type, }; use bumpalo::collections::string::String; use bumpalo::collections::vec::Vec; @@ -54,20 +52,6 @@ fn tag_union_type<'a>(min_indent: u16) -> impl Parser<'a, TypeAnnotation<'a>, TT } } -fn check_indent<'a, TE, E>(min_indent: u16, to_problem: TE) -> impl Parser<'a, (), E> -where - TE: Fn(Row, Col) -> E, - E: 'a, -{ - move |_arena, state: State<'a>| { - if state.indent_col < min_indent { - Err((NoProgress, to_problem(state.line, state.column), state)) - } else { - Ok((NoProgress, (), state)) - } - } -} - #[allow(clippy::type_complexity)] fn term<'a>(min_indent: u16) -> impl Parser<'a, Located>, SyntaxError<'a>> { specialize(|x, _, _| SyntaxError::Type(x), term_help(min_indent)) @@ -247,7 +231,7 @@ fn record_type_field<'a>( debug_assert_eq!(progress, MadeProgress); let (_, spaces, state) = - space0_e(min_indent, TRecord::Space, TRecord::IndentEnd).parse(arena, state)?; + debug!(space0_e(min_indent, TRecord::Space, TRecord::IndentEnd)).parse(arena, state)?; // Having a value is optional; both `{ email }` and `{ email: blah }` work. // (This is true in both literals and types.) @@ -312,9 +296,11 @@ fn record_type<'a>(min_indent: u16) -> impl Parser<'a, TypeAnnotation<'a>, TReco move |arena, state| { let (_, (fields, final_comments), state) = collection_trailing_sep_e!( + // word1_check_indent!(b'{', TRecord::Open, min_indent, TRecord::IndentOpen), word1(b'{', TRecord::Open), loc!(record_type_field(min_indent)), word1(b',', TRecord::End), + // word1_check_indent!(b'}', TRecord::End, min_indent, TRecord::IndentEnd), word1(b'}', TRecord::End), min_indent, TRecord::Open, @@ -377,13 +363,20 @@ fn expression<'a>(min_indent: u16) -> impl Parser<'a, Located .parse(arena, state)?; let (p2, rest, state) = zero_or_more!(skip_first!( - word1(b',', Type::TStart), - space0_around_e( - term_help(min_indent), - min_indent, - Type::TSpace, - Type::TIndentStart - ) + word1(b',', Type::TFunctionArgument), + one_of![ + space0_around_e( + term_help(min_indent), + min_indent, + Type::TSpace, + Type::TIndentStart + ), + |_, state: State<'a>| Err(( + NoProgress, + Type::TFunctionArgument(state.line, state.column), + state + )) + ] )) .parse(arena, state)?; diff --git a/compiler/reporting/src/error/parse.rs b/compiler/reporting/src/error/parse.rs index 4c0c9911d8..07f6dd654e 100644 --- a/compiler/reporting/src/error/parse.rs +++ b/compiler/reporting/src/error/parse.rs @@ -102,6 +102,24 @@ fn to_syntax_report<'a>( report(doc) } + SyntaxError::Eof(region) => { + let doc = alloc.stack(vec![alloc.reflow("End of Field"), alloc.region(*region)]); + + Report { + filename, + doc, + title: "PARSE PROBLEM".to_string(), + } + } + SyntaxError::OutdentedTooFar => { + let doc = alloc.stack(vec![alloc.reflow("OutdentedTooFar")]); + + Report { + filename, + doc, + title: "PARSE PROBLEM".to_string(), + } + } Type(typ) => to_type_report(alloc, filename, &typ, 0, 0), _ => todo!("unhandled parse error: {:?}", parse_problem), } @@ -126,6 +144,43 @@ fn to_type_report<'a>( } Type::TApply(tapply, row, col) => to_tapply_report(alloc, filename, &tapply, *row, *col), + Type::TFunctionArgument(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 just started parsing a function argument type, but I encounterd two commas in a row:"), + alloc.region_with_subregion(surroundings, region), + alloc.concat(vec![alloc.reflow("Try removing one of them.")]), + ]); + + Report { + filename, + doc, + title: "DOUBLE COMMA".to_string(), + } + } + _ => todo!(), + }, + + Type::TStart(row, col) => { + 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 just started parsing a type, but I got stuck here:"), + alloc.region_with_subregion(surroundings, region), + alloc.note("I may be confused by indentation"), + ]); + + Report { + filename, + doc, + title: "UNFINISHED TYPE".to_string(), + } + } + Type::TIndentStart(row, col) => { let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col); let region = Region::from_row_col(*row, *col); @@ -342,7 +397,7 @@ fn to_trecord_report<'a>( } TRecord::IndentEnd(row, col) => { - match next_line_starts_with_close_curly(alloc.src_lines, row - 1) { + match next_line_starts_with_close_curly(alloc.src_lines, row.saturating_sub(1)) { Some((curly_row, curly_col)) => { let surroundings = Region::from_rows_cols(start_row, start_col, curly_row, curly_col); diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index a66ede5d2f..3e8f1df52e 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -4246,8 +4246,7 @@ mod test_reporting { I am partway through parsing a record type, but I got stuck here: 1│ f : { - 2│ foo : I64, - ^ + ^ I was expecting to see a closing curly brace before this, so try adding a } and see if that helps? @@ -4589,8 +4588,115 @@ mod test_reporting { I just started parsing an inline type alias, but I got stuck here: 1│ f : I64 as - 2│ f = 0 - ^ + ^ + + Note: I may be confused by indentation + "# + ), + ) + } + + #[test] + fn type_double_comma() { + report_problem_as( + indoc!( + r#" + f : I64,,I64 -> I64 + f = 0 + + f + "# + ), + indoc!( + r#" + ── DOUBLE COMMA ──────────────────────────────────────────────────────────────── + + I just started parsing a function argument type, but I encounterd two + commas in a row: + + 1│ f : I64,,I64 -> I64 + ^ + + Try removing one of them. + "# + ), + ) + } + + #[test] + fn type_argument_no_arrow() { + report_problem_as( + indoc!( + r#" + f : I64, I64 + f = 0 + + f + "# + ), + indoc!( + r#" + ── UNFINISHED TYPE ───────────────────────────────────────────────────────────── + + I just started parsing a type, but I got stuck here: + + 1│ f : I64, I64 + ^ + + Note: I may be confused by indentation + "# + ), + ) + } + + #[test] + fn type_argument_arrow_then_nothing() { + // TODO could do better by pointing out we're parsing a function type + report_problem_as( + indoc!( + r#" + f : I64, I64 -> + f = 0 + + f + "# + ), + indoc!( + r#" + ── UNFINISHED TYPE ───────────────────────────────────────────────────────────── + + I just started parsing a type, but I got stuck here: + + 1│ f : I64, I64 -> + ^ + + Note: I may be confused by indentation + "# + ), + ) + } + + #[test] + fn foobar() { + // TODO fix error on new row + // we should make whitespace only consumed when it puts us in a validly-indented position + report_problem_as( + indoc!( + r#" + f : I64 -> + f = 0 + + f + "# + ), + indoc!( + r#" + ── UNFINISHED TYPE ───────────────────────────────────────────────────────────── + + I just started parsing a type, but I got stuck here: + + 1│ f : I64 -> + ^ Note: I may be confused by indentation "#