fiddling with the when parser

This commit is contained in:
Folkert 2021-03-24 22:55:01 +01:00
parent dd8bdcb806
commit 2a0c5c669b
9 changed files with 538 additions and 130 deletions

View File

@ -28,7 +28,7 @@ mod test_fmt {
assert_eq!(buf, expected)
}
Err(error) => panic!("Unexpected parse failure when parsing this for formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", input, error)
Err(error) => panic!("Unexpected parse failure when parsing this for formatting:\n\n{}\n\nParse error was:\n\n{:?}\n\n", input, error)
};
}

View File

@ -125,7 +125,7 @@ where
E: 'a,
{
move |_, state: State<'a>| {
if state.column > min_indent {
if state.column >= min_indent {
Ok((NoProgress, (), state))
} else {
Err((NoProgress, indent_problem(state.line, state.column), state))

View File

@ -16,16 +16,33 @@ use roc_region::all::{Located, Position, Region};
use crate::parser::Progress::{self, *};
fn expr_end<'a>() -> impl Parser<'a, (), EExpr<'a>> {
|_arena, state: State<'a>| {
if state.has_reached_end() {
Ok((NoProgress, (), state))
} else {
Err((
NoProgress,
EExpr::BadExprEnd(state.line, state.column),
state,
))
}
}
}
pub fn test_parse_expr<'a>(
min_indent: u16,
arena: &'a bumpalo::Bump,
state: State<'a>,
) -> Result<Located<Expr<'a>>, EExpr<'a>> {
let parser = space0_before_e(
let parser = skip_second!(
space0_before_e(
move |a, s| parse_loc_expr(min_indent, a, s),
min_indent,
EExpr::Space,
EExpr::IndentStart,
),
expr_end()
);
match parser.parse(arena, state) {
@ -1593,15 +1610,13 @@ mod when {
// 1. Parse the first branch and get its indentation level. (It must be >= min_indent.)
// 2. Parse the other branches. Their indentation levels must be == the first branch's.
let (_, (loc_first_patterns, loc_first_guard), state) =
branch_alternatives(min_indent).parse(arena, state)?;
let loc_first_pattern = loc_first_patterns.first().unwrap();
let original_indent = loc_first_pattern.region.start_col;
let indented_more = original_indent + 1;
let (_, ((pattern_indent_level, loc_first_patterns), loc_first_guard), state) =
branch_alternatives(min_indent, None).parse(arena, state)?;
let original_indent = pattern_indent_level;
// Parse the first "->" and the expression after it.
let (_, loc_first_expr, mut state) =
branch_result(indented_more).parse(arena, state)?;
branch_result(original_indent + 1).parse(arena, state)?;
// Record this as the first branch, then optionally parse additional branches.
branches.push(arena.alloc(WhenBranch {
@ -1613,19 +1628,21 @@ mod when {
let branch_parser = map!(
and!(
then(
branch_alternatives(min_indent),
move |_arena, state, _, (loc_patterns, loc_guard)| {
match alternatives_indented_correctly(&loc_patterns, original_indent) {
Ok(()) => Ok((MadeProgress, (loc_patterns, loc_guard), state)),
Err(indent) => Err((
branch_alternatives(min_indent, Some(pattern_indent_level)),
move |_arena, state, _, ((indent_col, loc_patterns), loc_guard)| {
if pattern_indent_level == indent_col {
Ok((MadeProgress, (loc_patterns, loc_guard), state))
} else {
let indent = pattern_indent_level - indent_col;
Err((
MadeProgress,
When::PatternAlignment(indent, state.line, state.column),
state,
)),
))
}
},
),
branch_result(indented_more)
branch_result(original_indent + 1)
),
|((patterns, guard), expr)| {
let patterns: Vec<'a, _> = patterns;
@ -1662,9 +1679,43 @@ mod when {
/// Parsing alternative patterns in when branches.
fn branch_alternatives<'a>(
min_indent: u16,
) -> impl Parser<'a, (Vec<'a, Located<Pattern<'a>>>, Option<Located<Expr<'a>>>), When<'a>> {
pattern_indent_level: Option<u16>,
) -> impl Parser<
'a,
(
(Col, Vec<'a, Located<Pattern<'a>>>),
Option<Located<Expr<'a>>>,
),
When<'a>,
> {
and!(
sep_by1(word1(b'|', When::Bar), |arena, state| {
branch_alternatives_help(min_indent, pattern_indent_level),
one_of![
map!(
skip_first!(
parser::keyword_e(keyword::IF, When::IfToken),
// TODO we should require space before the expression but not after
space0_around_ee(
specialize_ref(When::IfGuard, move |arena, state| {
parse_loc_expr(min_indent + 1, arena, state)
}),
min_indent,
When::Space,
When::IndentIfGuard,
When::IndentArrow,
)
),
Some
),
|_, s| Ok((NoProgress, None, s))
]
)
}
fn branch_single_alternative<'a>(
min_indent: u16,
) -> impl Parser<'a, Located<Pattern<'a>>, When<'a>> {
move |arena, state| {
let (_, spaces, state) =
backtrackable(space0_e(min_indent, When::Space, When::IndentPattern))
.parse(arena, state)?;
@ -1688,45 +1739,76 @@ mod when {
},
state,
))
}),
one_of![
map!(
skip_first!(
parser::keyword_e(keyword::IF, When::IfToken),
// TODO we should require space before the expression but not after
space0_around_ee(
specialize_ref(When::IfGuard, move |arena, state| {
parse_loc_expr(min_indent, arena, state)
}),
min_indent,
When::Space,
When::IndentIfGuard,
When::IndentArrow,
)
),
Some
),
|_, s| Ok((NoProgress, None, s))
]
)
}
}
/// Check if alternatives of a when branch are indented correctly.
fn alternatives_indented_correctly<'a>(
loc_patterns: &'a Vec<'a, Located<Pattern<'a>>>,
original_indent: u16,
) -> Result<(), u16> {
let (first, rest) = loc_patterns.split_first().unwrap();
let first_indented_correctly = first.region.start_col == original_indent;
if first_indented_correctly {
for when_pattern in rest.iter() {
if when_pattern.region.start_col < original_indent {
return Err(original_indent - when_pattern.region.start_col);
fn branch_alternatives_help<'a>(
min_indent: u16,
pattern_indent_level: Option<u16>,
) -> impl Parser<'a, (Col, Vec<'a, Located<Pattern<'a>>>), When<'a>> {
move |arena, state: State<'a>| {
let initial = state;
// put no restrictions on the indent after the spaces; we'll check it manually
match space0_e(0, When::Space, When::IndentPattern).parse(arena, state) {
Err((MadeProgress, fail, _)) => Err((NoProgress, fail, initial)),
Err((NoProgress, fail, _)) => Err((NoProgress, fail, initial)),
Ok((_, spaces, state)) => {
match pattern_indent_level {
Some(wanted) if state.column > wanted => {
// this branch is indented too much
Err((
MadeProgress,
When::IndentPattern(state.line, state.column),
state,
))
}
Some(wanted) if state.column < wanted => {
let indent = wanted - state.column;
Err((
NoProgress,
When::PatternAlignment(indent, state.line, state.column),
initial,
))
}
_ => {
let pattern_indent =
min_indent.max(pattern_indent_level.unwrap_or(min_indent));
// the region is not reliable for the indent col in the case of
// parentheses around patterns
let pattern_indent_col = state.column;
let parser = sep_by1(
word1(b'|', When::Bar),
branch_single_alternative(pattern_indent + 1),
);
match parser.parse(arena, state) {
Err((MadeProgress, fail, state)) => {
Err((MadeProgress, fail, state))
}
Err((NoProgress, fail, _)) => {
// roll back space parsing if the pattern made no progress
Err((NoProgress, fail, initial))
}
Ok((_, mut loc_patterns, state)) => {
// tag spaces onto the first parsed pattern
if !spaces.is_empty() {
if let Some(first) = loc_patterns.get_mut(0) {
*first = arena
.alloc(first.value)
.with_spaces_before(spaces, first.region);
}
}
Ok((MadeProgress, (pattern_indent_col, loc_patterns), state))
}
}
}
}
}
}
Ok(())
} else {
Err(original_indent - first.region.start_col)
}
}

View File

@ -379,6 +379,7 @@ pub type Col = u16;
pub enum EExpr<'a> {
Start(Row, Col),
End(Row, Col),
BadExprEnd(Row, Col),
Space(BadInputError, Row, Col),
Dot(Row, Col),
@ -926,8 +927,8 @@ where
state = next_state;
buf.push(next_output);
}
Err((element_progress, fail, state)) => {
return Err((element_progress, fail, state));
Err((_, fail, state)) => {
return Err((MadeProgress, fail, state));
}
}
}

View File

@ -62,8 +62,8 @@ pub fn loc_pattern_help<'a>(
EPattern::Record,
crate::pattern::record_pattern_help(min_indent)
)),
loc!(number_pattern_help()),
loc!(string_pattern_help()),
loc!(number_pattern_help())
)
}

View File

@ -2550,6 +2550,201 @@ mod test_parse {
assert_eq!(Ok(expected), actual);
}
#[test]
fn when_with_negative_numbers() {
let arena = Bump::new();
let newlines = &[Newline];
let pattern1 = Pattern::SpaceBefore(arena.alloc(NumLiteral("1")), newlines);
let loc_pattern1 = Located::new(1, 1, 1, 2, pattern1);
let expr1 = Num("2");
let loc_expr1 = Located::new(1, 1, 6, 7, expr1);
let branch1 = &*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern1]),
value: loc_expr1,
guard: None,
});
let newlines = &[Newline];
let pattern2 = Pattern::SpaceBefore(arena.alloc(NumLiteral("-3")), newlines);
let loc_pattern2 = Located::new(2, 2, 1, 3, pattern2);
let expr2 = Num("4");
let loc_expr2 = Located::new(2, 2, 7, 8, expr2);
let branch2 = &*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern2]),
value: loc_expr2,
guard: None,
});
let branches = &[branch1, branch2];
let var = Var {
module_name: "",
ident: "x",
};
let loc_cond = Located::new(0, 0, 5, 6, var);
let expected = Expr::When(arena.alloc(loc_cond), branches);
let actual = parse_expr_with(
&arena,
indoc!(
r#"
when x is
1 -> 2
-3 -> 4
"#
),
);
assert_eq!(Ok(expected), actual);
}
#[test]
fn when_with_function_application() {
let arena = Bump::new();
let newlines = &[Newline];
let pattern1 = Pattern::SpaceBefore(arena.alloc(NumLiteral("1")), newlines);
let loc_pattern1 = Located::new(1, 1, 4, 5, pattern1);
let num_2 = Num("2");
let num_neg = Located::new(
1,
1,
9,
16,
Expr::Var {
module_name: "Num",
ident: "neg",
},
);
let expr0 = Located::new(2, 2, 5, 6, Expr::SpaceBefore(&num_2, &[Newline]));
let expr1 = Expr::Apply(
&num_neg,
&*arena.alloc([&*arena.alloc(expr0)]),
CalledVia::Space,
);
let loc_expr1 = Located::new(1, 2, 9, 6, expr1);
let branch1 = &*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern1]),
value: loc_expr1,
guard: None,
});
let newlines = &[Newline];
let pattern2 = Pattern::SpaceBefore(arena.alloc(Underscore("")), newlines);
let loc_pattern2 = Located::new(3, 3, 4, 5, pattern2);
let expr2 = Num("4");
let loc_expr2 = Located::new(3, 3, 9, 10, expr2);
let branch2 = &*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern2]),
value: loc_expr2,
guard: None,
});
let branches = &[branch1, branch2];
let var = Var {
module_name: "",
ident: "x",
};
let loc_cond = Located::new(0, 0, 5, 6, var);
let expected = Expr::When(arena.alloc(loc_cond), branches);
let actual = parse_expr_with(
&arena,
indoc!(
r#"
when x is
1 -> Num.neg
2
_ -> 4
"#
),
);
assert_eq!(Ok(expected), actual);
}
#[test]
fn when_if_guard() {
let arena = Bump::new();
let branch1 = {
let newlines = &[Newline];
let pattern1 = Pattern::SpaceBefore(arena.alloc(Underscore("")), newlines);
let loc_pattern1 = Located::new(1, 1, 4, 5, pattern1);
let num_1 = Num("1");
let expr1 = Located::new(
2,
2,
8,
9,
Expr::SpaceBefore(arena.alloc(num_1), &[Newline]),
);
let loc_expr1 = expr1; // Located::new(1, 2, 9, 6, expr1);
&*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern1]),
value: loc_expr1,
guard: None,
})
};
let branch2 = {
let pattern1 = Pattern::SpaceBefore(arena.alloc(Underscore("")), &[Newline, Newline]);
let loc_pattern1 = Located::new(4, 4, 4, 5, pattern1);
let num_1 = Num("2");
let expr1 = Located::new(
5,
5,
8,
9,
Expr::SpaceBefore(arena.alloc(num_1), &[Newline]),
);
let loc_expr1 = expr1; // Located::new(1, 2, 9, 6, expr1);
&*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern1]),
value: loc_expr1,
guard: None,
})
};
let branch3 = {
let pattern1 =
Pattern::SpaceBefore(arena.alloc(Pattern::GlobalTag("Ok")), &[Newline, Newline]);
let loc_pattern1 = Located::new(7, 7, 4, 6, pattern1);
let num_1 = Num("3");
let expr1 = Located::new(
8,
8,
8,
9,
Expr::SpaceBefore(arena.alloc(num_1), &[Newline]),
);
let loc_expr1 = expr1; // Located::new(1, 2, 9, 6, expr1);
&*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern1]),
value: loc_expr1,
guard: None,
})
};
let branches = &[branch1, branch2, branch3];
let var = Var {
module_name: "",
ident: "x",
};
let loc_cond = Located::new(0, 0, 5, 6, var);
let expected = Expr::When(arena.alloc(loc_cond), branches);
let actual = parse_expr_with(
&arena,
indoc!(
r#"
when x is
_ ->
1
_ ->
2
Ok ->
3
"#
),
);
assert_eq!(Ok(expected), actual);
}
#[test]
fn when_with_records() {
let arena = Bump::new();
@ -2620,9 +2815,9 @@ mod test_parse {
let pattern2_alt =
Pattern::SpaceBefore(arena.alloc(StrLiteral(PlainLine("bar"))), newlines);
let loc_pattern2 = Located::new(2, 2, 1, 6, pattern2);
let loc_pattern2_alt = Located::new(3, 3, 1, 6, pattern2_alt);
let loc_pattern2_alt = Located::new(3, 3, 2, 7, pattern2_alt);
let expr2 = Num("2");
let loc_expr2 = Located::new(3, 3, 10, 11, expr2);
let loc_expr2 = Located::new(3, 3, 11, 12, expr2);
let branch2 = &*arena.alloc(WhenBranch {
patterns: arena.alloc([loc_pattern2, loc_pattern2_alt]),
value: loc_expr2,

View File

@ -458,6 +458,27 @@ fn to_expr_report<'a>(
*col,
),
EExpr::BadExprEnd(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 got stuck here:"),
alloc.region_with_subregion(surroundings, region),
alloc.concat(vec![
alloc.reflow("Whatever I am running into is confusing me al lot! "),
alloc.reflow("Normally I can give fairly specific hints, "),
alloc.reflow("but something is really tripping me up this time."),
]),
]);
Report {
filename,
doc,
title: "SYNTAX PROBLEM".to_string(),
}
}
EExpr::Colon(row, col) => {
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
let region = Region::from_row_col(*row, *col);
@ -982,7 +1003,7 @@ fn to_list_report<'a>(
),
List::Open(row, col) | List::End(row, col) => {
match dbg!(what_is_next(alloc.src_lines, 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);
@ -1388,6 +1409,36 @@ fn to_unfinished_when_report<'a>(
start_col: Col,
message: RocDocBuilder<'a>,
) -> Report<'a> {
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_rows_cols(row, col, row, col + 2);
let doc = alloc.stack(vec![
alloc.concat(vec![
alloc.reflow(r"I am parsing a "),
alloc.keyword("when"),
alloc.reflow(r" expression right now, but this arrow is confusing me:"),
]),
alloc.region_with_subregion(surroundings, region),
alloc.concat(vec![
alloc.reflow(r"It makes sense to see arrows around here, "),
alloc.reflow(r"so I suspect it is something earlier."),
alloc.reflow(
r"Maybe this pattern is indented a bit farther from the previous patterns?",
),
]),
note_for_when_error(alloc),
]);
Report {
filename,
doc,
title: "UNEXPECTED ARROW".to_string(),
}
}
_ => {
let surroundings = Region::from_rows_cols(start_row, start_col, row, col);
let region = Region::from_row_col(row, col);
@ -1408,6 +1459,8 @@ fn to_unfinished_when_report<'a>(
title: "UNFINISHED WHEN".to_string(),
}
}
}
}
fn note_for_when_error<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> {
alloc.stack(vec![

View File

@ -4964,7 +4964,6 @@ mod test_reporting {
#[test]
fn empty_or_pattern() {
// this should get better with time
report_problem_as(
indoc!(
r#"
@ -4978,29 +4977,16 @@ mod test_reporting {
),
indoc!(
r#"
MISSING EXPRESSION
UNFINISHED PATTERN
I am partway through parsing a definition, but I got stuck here:
I just started parsing a pattern, but I got stuck here:
1 when Just 4 is
2 Just 4 | ->
^
I was expecting to see an expression like 42 or "hello".
Note: I may be confused by indentation
"#
),
// indoc!(
// r#"
// ── UNFINISHED PATTERN ──────────────────────────────────────────────────────────
//
// I just started parsing a pattern, but I got stuck here:
//
// 2│ Just 4 | ->
// ^
//
// Note: I may be confused by indentation
// "#
// ),
)
}
@ -5127,6 +5113,57 @@ mod test_reporting {
#[test]
fn when_outdented_branch() {
// this should get better with time
report_problem_as(
indoc!(
r#"
when 4 is
5 -> 2
2 -> 2
"#
),
indoc!(
r#"
SYNTAX PROBLEM
I got stuck here:
1 when 4 is
2 5 -> 2
^
Whatever I am running into is confusing me al lot! Normally I can give
fairly specific hints, but something is really tripping me up this
time.
"#
),
// TODO this formerly gave
//
// ── UNFINISHED WHEN ─────────────────────────────────────────────────────────────
//
// I was partway through parsing a `when` expression, but I got stuck here:
//
// 3│ _ -> 2
// ^
//
// I suspect this is a pattern that is not indented enough? (by 2 spaces)
//
// but that requires parsing the next pattern blindly, irrespective of indentation. Can
// we find an efficient solution that doesn't require parsing an extra pattern for
// every `when`, i.e. we want a good error message for the test case above, but for
// a valid `when`, we don't want to do extra work, e.g. here
//
// x
// when x is
// n -> n
//
// 4
//
// We don't want to parse the `4` and say it's an outdented pattern!
)
}
#[test]
fn when_over_indented_underscore() {
report_problem_as(
indoc!(
r#"
@ -5144,7 +5181,47 @@ mod test_reporting {
3 _ -> 2
^
I suspect this is a pattern that is not indented enough? (by 2 spaces)
I was expecting to see a pattern next
Note: Here is an example of a valid `when` expression for reference.
when List.first plants is
Ok n ->
n
Err _ ->
200
Notice the indentation. All patterns are aligned, and each branch is
indented a bit more than the corresponding pattern. That is important!
"#
),
)
}
#[test]
fn when_over_indented_int() {
report_problem_as(
indoc!(
r#"
when 4 is
5 -> Num.neg
2 -> 2
"#
),
indoc!(
r#"
UNEXPECTED ARROW
I am parsing a `when` expression right now, but this arrow is confusing
me:
3 2 -> 2
^^
It makes sense to see arrows around here, so I suspect it is something
earlier.Maybe this pattern is indented a bit farther from the previous
patterns?
Note: Here is an example of a valid `when` expression for reference.

View File

@ -1,5 +1,5 @@
use roc_docs::{documentation_to_template_data, files_to_documentations, ModuleEntry};
use std::path::{Path, PathBuf};
use std::path::PathBuf;
#[cfg(test)]
mod test_docs {