diff --git a/crates/ast/src/lang/core/pattern.rs b/crates/ast/src/lang/core/pattern.rs index 6775e45b31..9037d52560 100644 --- a/crates/ast/src/lang/core/pattern.rs +++ b/crates/ast/src/lang/core/pattern.rs @@ -447,6 +447,7 @@ pub fn to_pattern2<'a>( unreachable!("should have been handled in RecordDestructure"); } + Tuple(..) => todo!(), List(..) => todo!(), ListRest => todo!(), diff --git a/crates/compiler/can/src/pattern.rs b/crates/compiler/can/src/pattern.rs index e2e96ee52d..a795f255af 100644 --- a/crates/compiler/can/src/pattern.rs +++ b/crates/compiler/can/src/pattern.rs @@ -529,6 +529,11 @@ pub fn canonicalize_pattern<'a>( permit_shadows, ) } + + Tuple(_patterns) => { + todo!("canonicalize_pattern: Tuple") + } + RecordDestructure(patterns) => { let ext_var = var_store.fresh(); let whole_var = var_store.fresh(); diff --git a/crates/compiler/fmt/src/pattern.rs b/crates/compiler/fmt/src/pattern.rs index 897d4ff767..0e380e6dc0 100644 --- a/crates/compiler/fmt/src/pattern.rs +++ b/crates/compiler/fmt/src/pattern.rs @@ -43,7 +43,9 @@ impl<'a> Formattable for Pattern<'a> { | Pattern::QualifiedIdentifier { .. } | Pattern::ListRest => false, - Pattern::List(patterns) => patterns.iter().any(|p| p.is_multiline()), + Pattern::Tuple(patterns) | Pattern::List(patterns) => { + patterns.iter().any(|p| p.is_multiline()) + } } } @@ -162,6 +164,22 @@ impl<'a> Formattable for Pattern<'a> { buf.push('_'); buf.push_str(name); } + Tuple(loc_patterns) => { + buf.indent(indent); + buf.push_str("("); + + let mut it = loc_patterns.iter().peekable(); + while let Some(loc_pattern) = it.next() { + loc_pattern.format(buf, indent); + + if it.peek().is_some() { + buf.push_str(","); + buf.spaces(1); + } + } + + buf.push_str(")"); + } List(loc_patterns) => { buf.indent(indent); buf.push_str("["); diff --git a/crates/compiler/fmt/src/spaces.rs b/crates/compiler/fmt/src/spaces.rs index 648f6e1069..5b83fd43dc 100644 --- a/crates/compiler/fmt/src/spaces.rs +++ b/crates/compiler/fmt/src/spaces.rs @@ -753,6 +753,7 @@ impl<'a> RemoveSpaces<'a> for Pattern<'a> { Pattern::SpaceAfter(a, _) => a.remove_spaces(arena), Pattern::SingleQuote(a) => Pattern::SingleQuote(a), Pattern::List(pats) => Pattern::List(pats.remove_spaces(arena)), + Pattern::Tuple(pats) => Pattern::Tuple(pats.remove_spaces(arena)), Pattern::ListRest => Pattern::ListRest, } } diff --git a/crates/compiler/parse/src/ast.rs b/crates/compiler/parse/src/ast.rs index 0d41a3cf13..c952f75fce 100644 --- a/crates/compiler/parse/src/ast.rs +++ b/crates/compiler/parse/src/ast.rs @@ -669,6 +669,9 @@ pub enum Pattern<'a> { Underscore(&'a str), SingleQuote(&'a str), + /// A tuple pattern, e.g. (Just x, 1) + Tuple(Collection<'a, Loc>>), + /// A list pattern like [_, x, ..] List(Collection<'a, Loc>>), diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs index e68d2124ff..af719f90aa 100644 --- a/crates/compiler/parse/src/expr.rs +++ b/crates/compiler/parse/src/expr.rs @@ -1852,9 +1852,15 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result { - todo!("tuple patterns") - } + Expr::Tuple(fields) => Ok(Pattern::Tuple(fields.map_items_result( + arena, + |loc_expr| { + Ok(Loc { + region: loc_expr.region, + value: expr_to_pattern_help(arena, &loc_expr.value)?, + }) + }, + )?)), &Expr::Float(string) => Ok(Pattern::FloatLiteral(string)), &Expr::Num(string) => Ok(Pattern::NumLiteral(string)), diff --git a/crates/compiler/parse/src/parser.rs b/crates/compiler/parse/src/parser.rs index dc18f83674..6ce086b915 100644 --- a/crates/compiler/parse/src/parser.rs +++ b/crates/compiler/parse/src/parser.rs @@ -605,6 +605,7 @@ pub enum PList<'a> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum PInParens<'a> { + Empty(Position), End(Position), Open(Position), Pattern(&'a EPattern<'a>, Position), diff --git a/crates/compiler/parse/src/pattern.rs b/crates/compiler/parse/src/pattern.rs index 25bca1b944..dc55c3dafc 100644 --- a/crates/compiler/parse/src/pattern.rs +++ b/crates/compiler/parse/src/pattern.rs @@ -1,5 +1,5 @@ use crate::ast::{Has, Pattern}; -use crate::blankspace::{space0_around_ee, space0_before_e, space0_e}; +use crate::blankspace::{space0_before_e, space0_e}; use crate::ident::{lowercase_ident, parse_ident, Ident}; use crate::parser::Progress::{self, *}; use crate::parser::{ @@ -147,15 +147,37 @@ fn loc_parse_tag_pattern_arg<'a>( } fn loc_pattern_in_parens_help<'a>() -> impl Parser<'a, Loc>, PInParens<'a>> { - between!( - word1(b'(', PInParens::Open), - space0_around_ee( + then( + loc!(collection_trailing_sep_e!( + word1(b'(', PInParens::Open), specialize_ref(PInParens::Pattern, loc_pattern_help()), + word1(b',', PInParens::End), + word1(b')', PInParens::End), + PInParens::Open, PInParens::IndentOpen, - PInParens::IndentEnd, - ), - word1(b')', PInParens::End) + Pattern::SpaceBefore + )), + move |_arena, state, _, loc_elements| { + let elements = loc_elements.value; + let region = loc_elements.region; + + if elements.len() > 1 { + Ok(( + MadeProgress, + Loc::at(region, Pattern::Tuple(elements)), + state, + )) + } else if elements.is_empty() { + Err((NoProgress, PInParens::Empty(state.pos()), state)) + } else { + // TODO: don't discard comments before/after + // (stored in the Collection) + // TODO: add Pattern::ParensAround to faithfully represent the input + Ok((MadeProgress, elements.items[0], state)) + } + }, ) + .trace("pat_in_parens") } fn number_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { diff --git a/crates/compiler/parse/tests/snapshots/pass/annotated_tuple_destructure.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/annotated_tuple_destructure.expr.result-ast new file mode 100644 index 0000000000..01e228228f --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/annotated_tuple_destructure.expr.result-ast @@ -0,0 +1,87 @@ +Defs( + Defs { + tags: [ + Index(2147483649), + ], + regions: [ + @0-41, + ], + space_before: [ + Slice(start = 0, length = 0), + ], + space_after: [ + Slice(start = 0, length = 0), + ], + spaces: [], + type_defs: [], + value_defs: [ + Annotation( + @0-8 Tuple( + [ + @2-3 Identifier( + "x", + ), + @5-6 Identifier( + "y", + ), + ], + ), + @11-14 Apply( + "", + "Foo", + [], + ), + ), + AnnotatedBody { + ann_pattern: @0-8 Tuple( + [ + @2-3 Identifier( + "x", + ), + @5-6 Identifier( + "y", + ), + ], + ), + ann_type: @11-14 Apply( + "", + "Foo", + [], + ), + comment: None, + body_pattern: @15-23 Tuple( + [ + @17-18 Identifier( + "x", + ), + @20-21 Identifier( + "y", + ), + ], + ), + body_expr: @26-41 Tuple( + [ + @28-33 Str( + PlainLine( + "foo", + ), + ), + @35-39 Float( + "3.14", + ), + ], + ), + }, + ], + }, + @43-44 SpaceBefore( + Var { + module_name: "", + ident: "x", + }, + [ + Newline, + Newline, + ], + ), +) diff --git a/crates/compiler/parse/tests/snapshots/pass/annotated_tuple_destructure.expr.roc b/crates/compiler/parse/tests/snapshots/pass/annotated_tuple_destructure.expr.roc new file mode 100644 index 0000000000..5fd95cb9c1 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/annotated_tuple_destructure.expr.roc @@ -0,0 +1,4 @@ +( x, y ) : Foo +( x, y ) = ( "foo", 3.14 ) + +x diff --git a/crates/compiler/parse/tests/snapshots/pass/when_with_tuple_in_record.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/when_with_tuple_in_record.expr.result-ast new file mode 100644 index 0000000000..df339c30d1 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/when_with_tuple_in_record.expr.result-ast @@ -0,0 +1,94 @@ +When( + @5-18 Record( + [ + @6-17 RequiredValue( + @6-9 "foo", + [], + @11-17 Tuple( + [ + @12-13 Num( + "1", + ), + @15-16 Num( + "2", + ), + ], + ), + ), + ], + ), + [ + WhenBranch { + patterns: [ + @23-36 SpaceBefore( + RecordDestructure( + [ + @24-35 RequiredField( + "foo", + @29-35 Tuple( + [ + @30-31 NumLiteral( + "1", + ), + @33-34 Identifier( + "x", + ), + ], + ), + ), + ], + ), + [ + Newline, + ], + ), + ], + value: @40-41 Var { + module_name: "", + ident: "x", + }, + guard: None, + }, + WhenBranch { + patterns: [ + @43-56 SpaceBefore( + RecordDestructure( + [ + @44-55 RequiredField( + "foo", + @49-55 Tuple( + [ + @50-51 Underscore( + "", + ), + @53-54 Identifier( + "b", + ), + ], + ), + ), + ], + ), + [ + Newline, + ], + ), + ], + value: @60-65 BinOps( + [ + ( + @60-61 Num( + "3", + ), + @62-63 Plus, + ), + ], + @64-65 Var { + module_name: "", + ident: "b", + }, + ), + guard: None, + }, + ], +) diff --git a/crates/compiler/parse/tests/snapshots/pass/when_with_tuple_in_record.expr.roc b/crates/compiler/parse/tests/snapshots/pass/when_with_tuple_in_record.expr.roc new file mode 100644 index 0000000000..fd7f9cc536 --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/when_with_tuple_in_record.expr.roc @@ -0,0 +1,3 @@ +when {foo: (1, 2)} is + {foo: (1, x)} -> x + {foo: (_, b)} -> 3 + b diff --git a/crates/compiler/parse/tests/snapshots/pass/when_with_tuples.expr.result-ast b/crates/compiler/parse/tests/snapshots/pass/when_with_tuples.expr.result-ast new file mode 100644 index 0000000000..0422de764d --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/when_with_tuples.expr.result-ast @@ -0,0 +1,72 @@ +When( + @5-11 Tuple( + [ + @6-7 Num( + "1", + ), + @9-10 Num( + "2", + ), + ], + ), + [ + WhenBranch { + patterns: [ + @16-22 SpaceBefore( + Tuple( + [ + @17-18 NumLiteral( + "1", + ), + @20-21 Identifier( + "x", + ), + ], + ), + [ + Newline, + ], + ), + ], + value: @26-27 Var { + module_name: "", + ident: "x", + }, + guard: None, + }, + WhenBranch { + patterns: [ + @29-35 SpaceBefore( + Tuple( + [ + @30-31 Underscore( + "", + ), + @33-34 Identifier( + "b", + ), + ], + ), + [ + Newline, + ], + ), + ], + value: @39-44 BinOps( + [ + ( + @39-40 Num( + "3", + ), + @41-42 Plus, + ), + ], + @43-44 Var { + module_name: "", + ident: "b", + }, + ), + guard: None, + }, + ], +) diff --git a/crates/compiler/parse/tests/snapshots/pass/when_with_tuples.expr.roc b/crates/compiler/parse/tests/snapshots/pass/when_with_tuples.expr.roc new file mode 100644 index 0000000000..3bf4dd1afc --- /dev/null +++ b/crates/compiler/parse/tests/snapshots/pass/when_with_tuples.expr.roc @@ -0,0 +1,3 @@ +when (1, 2) is + (1, x) -> x + (_, b) -> 3 + b diff --git a/crates/compiler/parse/tests/test_parse.rs b/crates/compiler/parse/tests/test_parse.rs index 2537a96847..18d92a043c 100644 --- a/crates/compiler/parse/tests/test_parse.rs +++ b/crates/compiler/parse/tests/test_parse.rs @@ -305,6 +305,9 @@ mod test_parse { pass/zero_float.expr, pass/zero_int.expr, pass/basic_tuple.expr, + pass/when_with_tuples.expr, + pass/when_with_tuple_in_record.expr, + pass/annotated_tuple_destructure.expr, } fn snapshot_test( diff --git a/crates/reporting/src/error/parse.rs b/crates/reporting/src/error/parse.rs index d40f30119d..862cf903f2 100644 --- a/crates/reporting/src/error/parse.rs +++ b/crates/reporting/src/error/parse.rs @@ -2086,6 +2086,27 @@ fn to_pattern_in_parens_report<'a>( } } + PInParens::Empty(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a parenthesized pattern or tuple:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting to see a pattern next."), + alloc.reflow(r"Note, Roc doesn't use '()' as a null type."), + ]), + ]); + + Report { + filename, + doc, + title: "EMPTY PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + PInParens::End(pos) => { let surroundings = Region::new(start, pos); let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); diff --git a/crates/reporting/tests/test_reporting.rs b/crates/reporting/tests/test_reporting.rs index 348f8bdd43..04ba83e466 100644 --- a/crates/reporting/tests/test_reporting.rs +++ b/crates/reporting/tests/test_reporting.rs @@ -5947,12 +5947,12 @@ All branches in an `if` must have the same type! here: 4│ \( a - ^ + 5│ + 6│ + ^ I was expecting to see a closing parenthesis before this, so try adding a ) and see if that helps? - - Note: I may be confused by indentation "### ); @@ -5970,7 +5970,9 @@ All branches in an `if` must have the same type! here: 4│ \( a, - ^ + 5│ + 6│ + ^ I was expecting to see a closing parenthesis before this, so try adding a ) and see if that helps? @@ -5991,17 +5993,17 @@ All branches in an `if` must have the same type! here: 4│ \( a - ^ + 5│ + 6│ + ^ I was expecting to see a closing parenthesis before this, so try adding a ) and see if that helps? - - Note: I may be confused by indentation "### ); test_report!( - pattern_in_parens_indent_end, + unfinished_closure_pattern_in_parens, indoc!( r#" x = \( a @@ -6009,17 +6011,15 @@ All branches in an `if` must have the same type! "# ), @r###" - ── NEED MORE INDENTATION ─────────── tmp/pattern_in_parens_indent_end/Test.roc ─ + ── UNFINISHED FUNCTION ───── tmp/unfinished_closure_pattern_in_parens/Test.roc ─ - I am partway through parsing a pattern in parentheses, but I got stuck - here: + I was partway through parsing a function, but I got stuck here: 4│ x = \( a 5│ ) - ^ + ^ - I need this parenthesis to be indented more. Try adding more spaces - before it! + I just saw a pattern, so I was expecting to see a -> next. "### );