diff --git a/crates/cli/tests/cli/combine-tasks.roc b/crates/cli/tests/cli/combine-tasks.roc index 70778a21d4e..0e081c46447 100644 --- a/crates/cli/tests/cli/combine-tasks.roc +++ b/crates/cli/tests/cli/combine-tasks.roc @@ -9,8 +9,8 @@ main = a: Task.ok 123, b: Task.ok "abc", c: Task.ok [123], - d: Task.ok ["abc"], - e: Task.ok (Dict.single "a" "b"), + _d: Task.ok ["abc"], + _: Task.ok (Dict.single "a" "b"), }! Stdout.line! "For multiple tasks: $(Inspect.toStr multipleIn)" diff --git a/crates/cli/tests/cli_run.rs b/crates/cli/tests/cli_run.rs index d99d7ce1efa..25237266f28 100644 --- a/crates/cli/tests/cli_run.rs +++ b/crates/cli/tests/cli_run.rs @@ -958,7 +958,7 @@ mod cli_run { &[], &[], &[], - "For multiple tasks: {a: 123, b: \"abc\", c: [123], d: [\"abc\"], e: {\"a\": \"b\"}}\n", + "For multiple tasks: {a: 123, b: \"abc\", c: [123]}\n", UseValgrind::No, TestCliCommands::Run, ) diff --git a/crates/compiler/can/src/annotation.rs b/crates/compiler/can/src/annotation.rs index 2118faf72fb..54242dcd725 100644 --- a/crates/compiler/can/src/annotation.rs +++ b/crates/compiler/can/src/annotation.rs @@ -467,7 +467,8 @@ pub fn find_type_def_symbols( while let Some(assigned_field) = inner_stack.pop() { match assigned_field { AssignedField::RequiredValue(_, _, t) - | AssignedField::OptionalValue(_, _, t) => { + | AssignedField::OptionalValue(_, _, t) + | AssignedField::IgnoredValue(_, _, t) => { stack.push(&t.value); } AssignedField::LabelOnly(_) => {} @@ -1386,6 +1387,7 @@ fn can_assigned_fields<'a>( break 'inner label; } + IgnoredValue(_, _, _) => unreachable!(), LabelOnly(loc_field_name) => { // Interpret { a, b } as { a : a, b : b } let field_name = Lowercase::from(loc_field_name.value); diff --git a/crates/compiler/can/src/def.rs b/crates/compiler/can/src/def.rs index 70d95a72ec7..0ae85642fef 100644 --- a/crates/compiler/can/src/def.rs +++ b/crates/compiler/can/src/def.rs @@ -631,7 +631,9 @@ fn canonicalize_claimed_ability_impl<'a>( // An error will already have been reported Err(()) } - AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => { + AssignedField::SpaceBefore(_, _) + | AssignedField::SpaceAfter(_, _) + | AssignedField::IgnoredValue(_, _, _) => { internal_error!("unreachable") } } diff --git a/crates/compiler/can/src/desugar.rs b/crates/compiler/can/src/desugar.rs index cba1b7df4dd..2056178b0d2 100644 --- a/crates/compiler/can/src/desugar.rs +++ b/crates/compiler/can/src/desugar.rs @@ -521,44 +521,68 @@ pub fn desugar_expr<'a>( }); } - let mut field_names = Vec::with_capacity_in(fields.len(), arena); - let mut field_vals = Vec::with_capacity_in(fields.len(), arena); + struct FieldData<'d> { + name: Loc<&'d str>, + value: &'d Loc>, + ignored: bool, + } + + let mut field_data = Vec::with_capacity_in(fields.len(), arena); for field in fields.items { - match desugar_field(arena, &field.value, src, line_info, module_path) { - AssignedField::RequiredValue(loc_name, _, loc_val) => { - field_names.push(loc_name); - field_vals.push(loc_val); - } - AssignedField::LabelOnly(loc_name) => { - field_names.push(loc_name); - field_vals.push(arena.alloc(Loc { - region: loc_name.region, - value: Expr::Var { - module_name: "", - ident: loc_name.value, - }, - })); - } - AssignedField::OptionalValue(loc_name, _, loc_val) => { - return arena.alloc(Loc { - region: loc_expr.region, - value: OptionalFieldInRecordBuilder(arena.alloc(loc_name), loc_val), - }); - } - AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => { - unreachable!("Should have been desugared in `desugar_field`") - } - AssignedField::Malformed(_name) => {} - } + let (name, value, ignored) = + match desugar_field(arena, &field.value, src, line_info, module_path) { + AssignedField::RequiredValue(loc_name, _, loc_val) => { + (loc_name, loc_val, false) + } + AssignedField::IgnoredValue(loc_name, _, loc_val) => { + (loc_name, loc_val, true) + } + AssignedField::LabelOnly(loc_name) => ( + loc_name, + &*arena.alloc(Loc { + region: loc_name.region, + value: Expr::Var { + module_name: "", + ident: loc_name.value, + }, + }), + false, + ), + AssignedField::OptionalValue(loc_name, _, loc_val) => { + return arena.alloc(Loc { + region: loc_expr.region, + value: OptionalFieldInRecordBuilder(arena.alloc(loc_name), loc_val), + }); + } + AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => { + unreachable!("Should have been desugared in `desugar_field`") + } + AssignedField::Malformed(_name) => continue, + }; + + field_data.push(FieldData { + name, + value, + ignored, + }); } - let closure_arg_from_field = |field: Loc<&'a str>| Loc { - region: field.region, - value: Pattern::Identifier { - ident: arena.alloc_str(&format!("#{}", field.value)), - }, - }; + let closure_arg_from_field = + |FieldData { + name, + value: _, + ignored, + }: &FieldData<'a>| Loc { + region: name.region, + value: if *ignored { + Pattern::Underscore(name.value) + } else { + Pattern::Identifier { + ident: arena.alloc_str(&format!("#{}", name.value)), + } + }, + }; let combiner_closure_in_region = |region| { let closure_body = Tuple(Collection::with_items( @@ -607,15 +631,15 @@ pub fn desugar_expr<'a>( }; let closure_args = { - if field_names.len() == 2 { + if field_data.len() == 2 { arena.alloc_slice_copy(&[ - closure_arg_from_field(field_names[0]), - closure_arg_from_field(field_names[1]), + closure_arg_from_field(&field_data[0]), + closure_arg_from_field(&field_data[1]), ]) } else { let second_to_last_arg = - closure_arg_from_field(field_names[field_names.len() - 2]); - let last_arg = closure_arg_from_field(field_names[field_names.len() - 1]); + closure_arg_from_field(&field_data[field_data.len() - 2]); + let last_arg = closure_arg_from_field(&field_data[field_data.len() - 1]); let mut second_arg = Pattern::Tuple(Collection::with_items( arena.alloc_slice_copy(&[second_to_last_arg, last_arg]), @@ -623,18 +647,18 @@ pub fn desugar_expr<'a>( let mut second_arg_region = Region::span_across(&second_to_last_arg.region, &last_arg.region); - for index in (1..(field_names.len() - 2)).rev() { + for index in (1..(field_data.len() - 2)).rev() { second_arg = Pattern::Tuple(Collection::with_items(arena.alloc_slice_copy(&[ - closure_arg_from_field(field_names[index]), + closure_arg_from_field(&field_data[index]), Loc::at(second_arg_region, second_arg), ]))); second_arg_region = - Region::span_across(&field_names[index].region, &second_arg_region); + Region::span_across(&field_data[index].name.region, &second_arg_region); } arena.alloc_slice_copy(&[ - closure_arg_from_field(field_names[0]), + closure_arg_from_field(&field_data[0]), Loc::at(second_arg_region, second_arg), ]) } @@ -642,22 +666,26 @@ pub fn desugar_expr<'a>( let record_val = Record(Collection::with_items( Vec::from_iter_in( - field_names.iter().map(|field_name| { - Loc::at( - field_name.region, - AssignedField::RequiredValue( - Loc::at(field_name.region, field_name.value), - &[], - arena.alloc(Loc::at( - field_name.region, - Expr::Var { - module_name: "", - ident: arena.alloc_str(&format!("#{}", field_name.value)), - }, - )), - ), - ) - }), + field_data + .iter() + .filter(|field| !field.ignored) + .map(|field| { + Loc::at( + field.name.region, + AssignedField::RequiredValue( + field.name, + &[], + arena.alloc(Loc::at( + field.name.region, + Expr::Var { + module_name: "", + ident: arena + .alloc_str(&format!("#{}", field.name.value)), + }, + )), + ), + ) + }), arena, ) .into_bump_slice(), @@ -671,14 +699,14 @@ pub fn desugar_expr<'a>( ), }); - if field_names.len() == 2 { + if field_data.len() == 2 { return arena.alloc(Loc { region: loc_expr.region, value: Apply( new_mapper, arena.alloc_slice_copy(&[ - field_vals[0], - field_vals[1], + field_data[0].value, + field_data[1].value, record_combiner_closure, ]), CalledVia::RecordBuilder, @@ -688,27 +716,30 @@ pub fn desugar_expr<'a>( let mut inner_combined = arena.alloc(Loc { region: Region::span_across( - &field_vals[field_names.len() - 2].region, - &field_vals[field_names.len() - 1].region, + &field_data[field_data.len() - 2].value.region, + &field_data[field_data.len() - 1].value.region, ), value: Apply( new_mapper, arena.alloc_slice_copy(&[ - field_vals[field_names.len() - 2], - field_vals[field_names.len() - 1], + field_data[field_data.len() - 2].value, + field_data[field_data.len() - 1].value, combiner_closure_in_region(loc_expr.region), ]), CalledVia::RecordBuilder, ), }); - for index in (1..(field_names.len() - 2)).rev() { + for index in (1..(field_data.len() - 2)).rev() { inner_combined = arena.alloc(Loc { - region: Region::span_across(&field_vals[index].region, &inner_combined.region), + region: Region::span_across( + &field_data[index].value.region, + &inner_combined.region, + ), value: Apply( new_mapper, arena.alloc_slice_copy(&[ - field_vals[index], + field_data[index].value, inner_combined, combiner_closure_in_region(loc_expr.region), ]), @@ -722,7 +753,7 @@ pub fn desugar_expr<'a>( value: Apply( new_mapper, arena.alloc_slice_copy(&[ - field_vals[0], + field_data[0].value, inner_combined, record_combiner_closure, ]), @@ -1095,6 +1126,14 @@ fn desugar_field<'a>( spaces, desugar_expr(arena, loc_expr, src, line_info, module_path), ), + IgnoredValue(loc_str, spaces, loc_expr) => IgnoredValue( + Loc { + value: loc_str.value, + region: loc_str.region, + }, + spaces, + desugar_expr(arena, loc_expr, src, line_info, module_path), + ), LabelOnly(loc_str) => { // Desugar { x } into { x: x } let loc_expr = Loc { diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs index 5d3f3e18eb8..22f8570a53c 100644 --- a/crates/compiler/can/src/expr.rs +++ b/crates/compiler/can/src/expr.rs @@ -1846,6 +1846,11 @@ fn canonicalize_field<'a>( field_region: Region::span_across(&label.region, &loc_expr.region), }), + // An ignored value, e.g. `{ _name: 123 }` + IgnoredValue(_, _, _) => { + internal_error!("Somehow an IgnoredValue record field was not desugared!"); + } + // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) LabelOnly(_) => { internal_error!("Somehow a LabelOnly record field was not desugared!"); @@ -2433,7 +2438,8 @@ pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool { } ast::Expr::Record(fields) => fields.iter().all(|loc_field| match loc_field.value { ast::AssignedField::RequiredValue(_label, loc_comments, loc_val) - | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) => { + | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) + | ast::AssignedField::IgnoredValue(_label, loc_comments, loc_val) => { loc_comments.is_empty() && is_valid_interpolation(&loc_val.value) } ast::AssignedField::Malformed(_) | ast::AssignedField::LabelOnly(_) => true, @@ -2481,7 +2487,8 @@ pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool { is_valid_interpolation(&update.value) && fields.iter().all(|loc_field| match loc_field.value { ast::AssignedField::RequiredValue(_label, loc_comments, loc_val) - | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) => { + | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) + | ast::AssignedField::IgnoredValue(_label, loc_comments, loc_val) => { loc_comments.is_empty() && is_valid_interpolation(&loc_val.value) } ast::AssignedField::Malformed(_) | ast::AssignedField::LabelOnly(_) => true, @@ -2514,7 +2521,8 @@ pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool { is_valid_interpolation(&mapper.value) && fields.iter().all(|loc_field| match loc_field.value { ast::AssignedField::RequiredValue(_label, loc_comments, loc_val) - | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) => { + | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) + | ast::AssignedField::IgnoredValue(_label, loc_comments, loc_val) => { loc_comments.is_empty() && is_valid_interpolation(&loc_val.value) } ast::AssignedField::Malformed(_) | ast::AssignedField::LabelOnly(_) => true, diff --git a/crates/compiler/fmt/src/annotation.rs b/crates/compiler/fmt/src/annotation.rs index 25076b59406..b279a8fc9c9 100644 --- a/crates/compiler/fmt/src/annotation.rs +++ b/crates/compiler/fmt/src/annotation.rs @@ -428,9 +428,9 @@ fn is_multiline_assigned_field_help(afield: &AssignedField<'_, T use self::AssignedField::*; match afield { - RequiredValue(_, spaces, ann) | OptionalValue(_, spaces, ann) => { - !spaces.is_empty() || ann.value.is_multiline() - } + RequiredValue(_, spaces, ann) + | OptionalValue(_, spaces, ann) + | IgnoredValue(_, spaces, ann) => !spaces.is_empty() || ann.value.is_multiline(), LabelOnly(_) => false, AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => true, Malformed(text) => text.chars().any(|c| c == '\n'), @@ -483,6 +483,24 @@ fn format_assigned_field_help( buf.spaces(1); ann.value.format(buf, indent); } + IgnoredValue(name, spaces, ann) => { + if is_multiline { + buf.newline(); + } + + buf.indent(indent); + buf.push('_'); + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + } + + buf.spaces(separator_spaces); + buf.push(':'); + buf.spaces(1); + ann.value.format(buf, indent); + } LabelOnly(name) => { if is_multiline { buf.newline(); diff --git a/crates/compiler/fmt/src/expr.rs b/crates/compiler/fmt/src/expr.rs index 7793bd125d8..04544ea2c6f 100644 --- a/crates/compiler/fmt/src/expr.rs +++ b/crates/compiler/fmt/src/expr.rs @@ -1529,6 +1529,23 @@ fn format_assigned_field_multiline( ann.value.format(buf, indent); buf.push(','); } + IgnoredValue(name, spaces, ann) => { + buf.newline(); + buf.indent(indent); + buf.push('_'); + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + buf.indent(indent); + } + + buf.push_str(separator_prefix); + buf.push_str(":"); + buf.spaces(1); + ann.value.format(buf, indent); + buf.push(','); + } LabelOnly(name) => { buf.newline(); buf.indent(indent); diff --git a/crates/compiler/load_internal/src/docs.rs b/crates/compiler/load_internal/src/docs.rs index a8aa4505d87..3b4e3c8362b 100644 --- a/crates/compiler/load_internal/src/docs.rs +++ b/crates/compiler/load_internal/src/docs.rs @@ -465,7 +465,8 @@ fn contains_unexposed_type( while let Some(field) = fields_to_process.pop() { match field { AssignedField::RequiredValue(_field, _spaces, loc_val) - | AssignedField::OptionalValue(_field, _spaces, loc_val) => { + | AssignedField::OptionalValue(_field, _spaces, loc_val) + | AssignedField::IgnoredValue(_field, _spaces, loc_val) => { if contains_unexposed_type(&loc_val.value, exposed_module_ids, module_ids) { return true; } @@ -721,7 +722,7 @@ fn record_field_to_doc( AssignedField::LabelOnly(label) => Some(RecordField::LabelOnly { name: label.value.to_string(), }), - AssignedField::Malformed(_) => None, + AssignedField::Malformed(_) | AssignedField::IgnoredValue(_, _, _) => None, } } diff --git a/crates/compiler/parse/src/ast.rs b/crates/compiler/parse/src/ast.rs index 36899d0d8dd..5844e18c7bc 100644 --- a/crates/compiler/parse/src/ast.rs +++ b/crates/compiler/parse/src/ast.rs @@ -681,9 +681,9 @@ fn is_when_branch_suffixed(branch: &WhenBranch<'_>) -> bool { fn is_assigned_value_suffixed<'a>(value: &AssignedField<'a, Expr<'a>>) -> bool { match value { - AssignedField::RequiredValue(_, _, a) | AssignedField::OptionalValue(_, _, a) => { - is_expr_suffixed(&a.value) - } + AssignedField::RequiredValue(_, _, a) + | AssignedField::OptionalValue(_, _, a) + | AssignedField::IgnoredValue(_, _, a) => is_expr_suffixed(&a.value), AssignedField::LabelOnly(_) => false, AssignedField::SpaceBefore(a, _) | AssignedField::SpaceAfter(a, _) => { is_assigned_value_suffixed(a) @@ -869,9 +869,9 @@ impl<'a, 'b> RecursiveValueDefIter<'a, 'b> { use AssignedField::*; match current { - RequiredValue(_, _, loc_val) | OptionalValue(_, _, loc_val) => { - break expr_stack.push(&loc_val.value) - } + RequiredValue(_, _, loc_val) + | OptionalValue(_, _, loc_val) + | IgnoredValue(_, _, loc_val) => break expr_stack.push(&loc_val.value), SpaceBefore(next, _) | SpaceAfter(next, _) => current = *next, LabelOnly(_) | Malformed(_) => break, } @@ -1598,6 +1598,9 @@ pub enum AssignedField<'a, Val> { // and in destructuring patterns (e.g. `{ name ? "blah" }`) OptionalValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc), + // An ignored field, e.g. `{ _name: "blah" }` or `{ _ : Str }` + IgnoredValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc), + // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) LabelOnly(Loc<&'a str>), @@ -1615,7 +1618,9 @@ impl<'a, Val> AssignedField<'a, Val> { loop { match current { - Self::RequiredValue(_, _, val) | Self::OptionalValue(_, _, val) => break Some(val), + Self::RequiredValue(_, _, val) + | Self::OptionalValue(_, _, val) + | Self::IgnoredValue(_, _, val) => break Some(val), Self::LabelOnly(_) | Self::Malformed(_) => break None, Self::SpaceBefore(next, _) | Self::SpaceAfter(next, _) => current = *next, } @@ -2577,9 +2582,9 @@ impl Malformed for Option { impl<'a, T: Malformed> Malformed for AssignedField<'a, T> { fn is_malformed(&self) -> bool { match self { - AssignedField::RequiredValue(_, _, val) | AssignedField::OptionalValue(_, _, val) => { - val.is_malformed() - } + AssignedField::RequiredValue(_, _, val) + | AssignedField::OptionalValue(_, _, val) + | AssignedField::IgnoredValue(_, _, val) => val.is_malformed(), AssignedField::LabelOnly(_) => false, AssignedField::SpaceBefore(field, _) | AssignedField::SpaceAfter(field, _) => { field.is_malformed() diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs index 03a4534cb30..1e5036c4981 100644 --- a/crates/compiler/parse/src/expr.rs +++ b/crates/compiler/parse/src/expr.rs @@ -914,6 +914,10 @@ fn import_params<'a>() -> impl Parser<'a, ModuleImportParams<'a>, EImportParams< let params = record.fields.map_items_result(arena, |loc_field| { match loc_field.value.to_assigned_field(arena) { + Ok(AssignedField::IgnoredValue(_, _, _)) => Err(( + MadeProgress, + EImportParams::RecordIgnoredFieldFound(loc_field.region), + )), Ok(field) => Ok(Loc::at(loc_field.region, field)), Err(FoundApplyValue) => Err(( MadeProgress, @@ -2234,6 +2238,7 @@ fn assigned_expr_field_to_pattern_help<'a>( spaces, ), AssignedField::Malformed(string) => Pattern::Malformed(string), + AssignedField::IgnoredValue(_, _, _) => return Err(()), }) } @@ -3322,6 +3327,7 @@ fn list_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EList<'a>> { pub enum RecordField<'a> { RequiredValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc>), OptionalValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc>), + IgnoredValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc>), LabelOnly(Loc<&'a str>), SpaceBefore(&'a RecordField<'a>, &'a [CommentOrNewline<'a>]), SpaceAfter(&'a RecordField<'a>, &'a [CommentOrNewline<'a>]), @@ -3337,7 +3343,10 @@ pub enum RecordField<'a> { pub struct FoundApplyValue; #[derive(Debug)] -struct FoundOptionalValue; +pub enum NotOldBuilderFieldValue { + FoundOptionalValue, + FoundIgnoredValue, +} impl<'a> RecordField<'a> { fn is_apply_value(&self) -> bool { @@ -3354,6 +3363,20 @@ impl<'a> RecordField<'a> { } } + fn is_ignored_value(&self) -> bool { + let mut current = self; + + loop { + match current { + RecordField::IgnoredValue(_, _, _) => break true, + RecordField::SpaceBefore(field, _) | RecordField::SpaceAfter(field, _) => { + current = *field; + } + _ => break false, + } + } + } + pub fn to_assigned_field( self, arena: &'a Bump, @@ -3369,6 +3392,10 @@ impl<'a> RecordField<'a> { Ok(OptionalValue(loc_label, spaces, loc_expr)) } + RecordField::IgnoredValue(loc_label, spaces, loc_expr) => { + Ok(IgnoredValue(loc_label, spaces, loc_expr)) + } + RecordField::LabelOnly(loc_label) => Ok(LabelOnly(loc_label)), RecordField::ApplyValue(_, _, _, _) => Err(FoundApplyValue), @@ -3390,7 +3417,7 @@ impl<'a> RecordField<'a> { fn to_builder_field( self, arena: &'a Bump, - ) -> Result, FoundOptionalValue> { + ) -> Result, NotOldBuilderFieldValue> { use OldRecordBuilderField::*; match self { @@ -3398,7 +3425,9 @@ impl<'a> RecordField<'a> { Ok(Value(loc_label, spaces, loc_expr)) } - RecordField::OptionalValue(_, _, _) => Err(FoundOptionalValue), + RecordField::OptionalValue(_, _, _) => Err(NotOldBuilderFieldValue::FoundOptionalValue), + + RecordField::IgnoredValue(_, _, _) => Err(NotOldBuilderFieldValue::FoundIgnoredValue), RecordField::LabelOnly(loc_label) => Ok(LabelOnly(loc_label)), @@ -3434,42 +3463,70 @@ pub fn record_field<'a>() -> impl Parser<'a, RecordField<'a>, ERecord<'a>> { use RecordField::*; map_with_arena( - and( - specialize_err(|_, pos| ERecord::Field(pos), loc(lowercase_ident())), + either( and( - spaces(), - optional(either( - and(byte(b':', ERecord::Colon), record_field_expr()), - and( - byte(b'?', ERecord::QuestionMark), + specialize_err(|_, pos| ERecord::Field(pos), loc(lowercase_ident())), + and( + spaces(), + optional(either( + and(byte(b':', ERecord::Colon), record_field_expr()), + and( + byte(b'?', ERecord::QuestionMark), + spaces_before(specialize_err_ref(ERecord::Expr, loc_expr(false))), + ), + )), + ), + ), + and( + loc(skip_first( + byte(b'_', ERecord::UnderscoreField), + optional(specialize_err( + |_, pos| ERecord::Field(pos), + lowercase_ident(), + )), + )), + and( + spaces(), + skip_first( + byte(b':', ERecord::Colon), spaces_before(specialize_err_ref(ERecord::Expr, loc_expr(false))), ), - )), + ), ), ), - |arena: &'a bumpalo::Bump, (loc_label, (spaces, opt_loc_val))| { - match opt_loc_val { - Some(Either::First((_, RecordFieldExpr::Value(loc_val)))) => { - RequiredValue(loc_label, spaces, arena.alloc(loc_val)) - } + |arena: &'a bumpalo::Bump, field_data| { + match field_data { + Either::First((loc_label, (spaces, opt_loc_val))) => { + match opt_loc_val { + Some(Either::First((_, RecordFieldExpr::Value(loc_val)))) => { + RequiredValue(loc_label, spaces, arena.alloc(loc_val)) + } - Some(Either::First((_, RecordFieldExpr::Apply(arrow_spaces, loc_val)))) => { - ApplyValue(loc_label, spaces, arrow_spaces, arena.alloc(loc_val)) - } + Some(Either::First((_, RecordFieldExpr::Apply(arrow_spaces, loc_val)))) => { + ApplyValue(loc_label, spaces, arrow_spaces, arena.alloc(loc_val)) + } - Some(Either::Second((_, loc_val))) => { - OptionalValue(loc_label, spaces, arena.alloc(loc_val)) - } + Some(Either::Second((_, loc_val))) => { + OptionalValue(loc_label, spaces, arena.alloc(loc_val)) + } - // If no value was provided, record it as a Var. - // Canonicalize will know what to do with a Var later. - None => { - if !spaces.is_empty() { - SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces) - } else { - LabelOnly(loc_label) + // If no value was provided, record it as a Var. + // Canonicalize will know what to do with a Var later. + None => { + if !spaces.is_empty() { + SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces) + } else { + LabelOnly(loc_label) + } + } } } + Either::Second((loc_opt_label, (spaces, loc_val))) => { + let loc_label = loc_opt_label + .map(|opt_label| opt_label.unwrap_or_else(|| arena.alloc_str(""))); + + IgnoredValue(loc_label, spaces, arena.alloc(loc_val)) + } } }, ) @@ -3573,20 +3630,23 @@ fn record_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { new_record_builder_help(arena, mapper, record.fields) } None => { - let is_old_record_builder = record - .fields - .iter() - .any(|field| field.value.is_apply_value()); + let special_field_found = record.fields.iter().find_map(|field| { + if field.value.is_apply_value() { + Some(old_record_builder_help(arena, record.fields)) + } else if field.value.is_ignored_value() { + Some(Err(EExpr::RecordUpdateIgnoredField(field.region))) + } else { + None + } + }); - if is_old_record_builder { - old_record_builder_help(arena, record.fields) - } else { + special_field_found.unwrap_or_else(|| { let fields = record.fields.map_items(arena, |loc_field| { loc_field.map(|field| field.to_assigned_field(arena).unwrap()) }); Ok(Expr::Record(fields)) - } + }) } }; @@ -3609,11 +3669,14 @@ fn record_update_help<'a>( ) -> Result, EExpr<'a>> { let result = fields.map_items_result(arena, |loc_field| { match loc_field.value.to_assigned_field(arena) { + Ok(AssignedField::IgnoredValue(_, _, _)) => { + Err(EExpr::RecordUpdateIgnoredField(loc_field.region)) + } Ok(builder_field) => Ok(Loc { region: loc_field.region, value: builder_field, }), - Err(FoundApplyValue) => Err(EExpr::RecordUpdateAccumulator(loc_field.region)), + Err(FoundApplyValue) => Err(EExpr::RecordUpdateOldBuilderField(loc_field.region)), } }); @@ -3634,7 +3697,7 @@ fn new_record_builder_help<'a>( region: loc_field.region, value: builder_field, }), - Err(FoundApplyValue) => Err(EExpr::RecordBuilderAccumulator(loc_field.region)), + Err(FoundApplyValue) => Err(EExpr::RecordBuilderOldBuilderField(loc_field.region)), } }); @@ -3654,7 +3717,12 @@ fn old_record_builder_help<'a>( region: loc_field.region, value: builder_field, }), - Err(FoundOptionalValue) => Err(EExpr::OptionalValueInRecordBuilder(loc_field.region)), + Err(NotOldBuilderFieldValue::FoundOptionalValue) => { + Err(EExpr::OptionalValueInOldRecordBuilder(loc_field.region)) + } + Err(NotOldBuilderFieldValue::FoundIgnoredValue) => { + Err(EExpr::IgnoredValueInOldRecordBuilder(loc_field.region)) + } } }); diff --git a/crates/compiler/parse/src/parser.rs b/crates/compiler/parse/src/parser.rs index 7eec1b83e9a..c773acab730 100644 --- a/crates/compiler/parse/src/parser.rs +++ b/crates/compiler/parse/src/parser.rs @@ -372,9 +372,11 @@ pub enum EExpr<'a> { InParens(EInParens<'a>, Position), Record(ERecord<'a>, Position), - OptionalValueInRecordBuilder(Region), - RecordUpdateAccumulator(Region), - RecordBuilderAccumulator(Region), + OptionalValueInOldRecordBuilder(Region), + IgnoredValueInOldRecordBuilder(Region), + RecordUpdateOldBuilderField(Region), + RecordUpdateIgnoredField(Region), + RecordBuilderOldBuilderField(Region), // SingleQuote errors are folded into the EString Str(EString<'a>, Position), @@ -428,6 +430,7 @@ pub enum ERecord<'a> { Prefix(Position), Field(Position), + UnderscoreField(Position), Colon(Position), QuestionMark(Position), Arrow(Position), @@ -577,6 +580,7 @@ pub enum EImportParams<'a> { RecordUpdateFound(Region), RecordBuilderFound(Region), RecordApplyFound(Region), + RecordIgnoredFieldFound(Region), Space(BadInputError, Position), } @@ -735,6 +739,7 @@ pub enum ETypeAbilityImpl<'a> { Open(Position), Field(Position), + UnderscoreField(Position), Colon(Position), Arrow(Position), Optional(Position), @@ -756,6 +761,7 @@ impl<'a> From> for ETypeAbilityImpl<'a> { ERecord::End(p) => ETypeAbilityImpl::End(p), ERecord::Open(p) => ETypeAbilityImpl::Open(p), ERecord::Field(p) => ETypeAbilityImpl::Field(p), + ERecord::UnderscoreField(p) => ETypeAbilityImpl::UnderscoreField(p), ERecord::Colon(p) => ETypeAbilityImpl::Colon(p), ERecord::Arrow(p) => ETypeAbilityImpl::Arrow(p), ERecord::Space(s, p) => ETypeAbilityImpl::Space(s, p), diff --git a/crates/compiler/parse/src/remove_spaces.rs b/crates/compiler/parse/src/remove_spaces.rs index 1de484706e7..67178b652bd 100644 --- a/crates/compiler/parse/src/remove_spaces.rs +++ b/crates/compiler/parse/src/remove_spaces.rs @@ -543,6 +543,11 @@ impl<'a, T: RemoveSpaces<'a> + Copy + std::fmt::Debug> RemoveSpaces<'a> for Assi arena.alloc([]), arena.alloc(c.remove_spaces(arena)), ), + AssignedField::IgnoredValue(a, _, c) => AssignedField::IgnoredValue( + a.remove_spaces(arena), + arena.alloc([]), + arena.alloc(c.remove_spaces(arena)), + ), AssignedField::LabelOnly(a) => AssignedField::LabelOnly(a.remove_spaces(arena)), AssignedField::Malformed(a) => AssignedField::Malformed(a), AssignedField::SpaceBefore(a, _) => a.remove_spaces(arena), @@ -983,8 +988,11 @@ impl<'a> RemoveSpaces<'a> for EExpr<'a> { EExpr::Record(inner_err, _pos) => { EExpr::Record(inner_err.remove_spaces(arena), Position::zero()) } - EExpr::OptionalValueInRecordBuilder(_pos) => { - EExpr::OptionalValueInRecordBuilder(Region::zero()) + EExpr::OptionalValueInOldRecordBuilder(_pos) => { + EExpr::OptionalValueInOldRecordBuilder(Region::zero()) + } + EExpr::IgnoredValueInOldRecordBuilder(_pos) => { + EExpr::OptionalValueInOldRecordBuilder(Region::zero()) } EExpr::Str(inner_err, _pos) => { EExpr::Str(inner_err.remove_spaces(arena), Position::zero()) @@ -998,8 +1006,15 @@ impl<'a> RemoveSpaces<'a> for EExpr<'a> { EExpr::UnexpectedComma(_pos) => EExpr::UnexpectedComma(Position::zero()), EExpr::UnexpectedTopLevelExpr(_pos) => EExpr::UnexpectedTopLevelExpr(Position::zero()), EExpr::StmtAfterExpr(_pos) => EExpr::StmtAfterExpr(Position::zero()), - EExpr::RecordUpdateAccumulator(_) => EExpr::RecordUpdateAccumulator(Region::zero()), - EExpr::RecordBuilderAccumulator(_) => EExpr::RecordBuilderAccumulator(Region::zero()), + EExpr::RecordUpdateOldBuilderField(_pos) => { + EExpr::RecordUpdateOldBuilderField(Region::zero()) + } + EExpr::RecordUpdateIgnoredField(_pos) => { + EExpr::RecordUpdateIgnoredField(Region::zero()) + } + EExpr::RecordBuilderOldBuilderField(_pos) => { + EExpr::RecordBuilderOldBuilderField(Region::zero()) + } } } } @@ -1089,6 +1104,7 @@ impl<'a> RemoveSpaces<'a> for ERecord<'a> { ERecord::End(_) => ERecord::End(Position::zero()), ERecord::Open(_) => ERecord::Open(Position::zero()), ERecord::Field(_pos) => ERecord::Field(Position::zero()), + ERecord::UnderscoreField(_pos) => ERecord::Field(Position::zero()), ERecord::Colon(_) => ERecord::Colon(Position::zero()), ERecord::QuestionMark(_) => ERecord::QuestionMark(Position::zero()), ERecord::Arrow(_) => ERecord::Arrow(Position::zero()), @@ -1217,6 +1233,9 @@ impl<'a> RemoveSpaces<'a> for EImportParams<'a> { } EImportParams::RecordUpdateFound(_) => EImportParams::RecordUpdateFound(Region::zero()), EImportParams::RecordApplyFound(_) => EImportParams::RecordApplyFound(Region::zero()), + EImportParams::RecordIgnoredFieldFound(_) => { + EImportParams::RecordIgnoredFieldFound(Region::zero()) + } EImportParams::Space(inner_err, _) => { EImportParams::Space(*inner_err, Position::zero()) } @@ -1248,6 +1267,9 @@ impl<'a> RemoveSpaces<'a> for ETypeAbilityImpl<'a> { ETypeAbilityImpl::End(_) => ETypeAbilityImpl::End(Position::zero()), ETypeAbilityImpl::Open(_) => ETypeAbilityImpl::Open(Position::zero()), ETypeAbilityImpl::Field(_) => ETypeAbilityImpl::Field(Position::zero()), + ETypeAbilityImpl::UnderscoreField(_) => { + ETypeAbilityImpl::UnderscoreField(Position::zero()) + } ETypeAbilityImpl::Colon(_) => ETypeAbilityImpl::Colon(Position::zero()), ETypeAbilityImpl::Arrow(_) => ETypeAbilityImpl::Arrow(Position::zero()), ETypeAbilityImpl::Optional(_) => ETypeAbilityImpl::Optional(Position::zero()), diff --git a/crates/compiler/parse/src/type_annotation.rs b/crates/compiler/parse/src/type_annotation.rs index 5bbc28903cd..2802a902e26 100644 --- a/crates/compiler/parse/src/type_annotation.rs +++ b/crates/compiler/parse/src/type_annotation.rs @@ -565,6 +565,9 @@ fn parse_implements_ability<'a>() -> impl Parser<'a, ImplementsAbility<'a>, ETyp fn ability_impl_field<'a>() -> impl Parser<'a, AssignedField<'a, Expr<'a>>, ERecord<'a>> { then(record_field(), move |arena, state, _, field| { match field.to_assigned_field(arena) { + Ok(AssignedField::IgnoredValue(_, _, _)) => { + Err((MadeProgress, ERecord::Field(state.pos()))) + } Ok(assigned_field) => Ok((MadeProgress, assigned_field, state)), Err(FoundApplyValue) => Err((MadeProgress, ERecord::Field(state.pos()))), } diff --git a/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.formatted.roc b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.formatted.roc new file mode 100644 index 00000000000..911a0f8c043 --- /dev/null +++ b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.formatted.roc @@ -0,0 +1,4 @@ +{ Foo.Bar.baz <- + x: 5, + y: 0, +} \ No newline at end of file diff --git a/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.result-ast b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.result-ast new file mode 100644 index 00000000000..b3d94466f17 --- /dev/null +++ b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.result-ast @@ -0,0 +1,32 @@ +SpaceAfter( + RecordBuilder { + mapper: @2-13 Var { + module_name: "Foo.Bar", + ident: "baz", + }, + fields: [ + @17-21 RequiredValue( + @17-18 "x", + [], + @20-21 Num( + "5", + ), + ), + @23-27 SpaceAfter( + RequiredValue( + @23-24 "y", + [], + @26-27 Num( + "0", + ), + ), + [ + Newline, + ], + ), + ], + }, + [ + Newline, + ], +) diff --git a/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.roc b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.roc new file mode 100644 index 00000000000..9a075a9e5c4 --- /dev/null +++ b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder.expr.roc @@ -0,0 +1,2 @@ +{ Foo.Bar.baz <- x: 5, y: 0 +} diff --git a/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.formatted.roc b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.formatted.roc new file mode 100644 index 00000000000..2bf71d9a449 --- /dev/null +++ b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.formatted.roc @@ -0,0 +1,6 @@ +{ Foo.Bar.baz <- + x: 5, + y: 0, + _z: 3, + _: 2, +} \ No newline at end of file diff --git a/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.result-ast b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.result-ast new file mode 100644 index 00000000000..af37c9c12e6 --- /dev/null +++ b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.result-ast @@ -0,0 +1,46 @@ +SpaceAfter( + RecordBuilder { + mapper: @2-13 Var { + module_name: "Foo.Bar", + ident: "baz", + }, + fields: [ + @17-21 RequiredValue( + @17-18 "x", + [], + @20-21 Num( + "5", + ), + ), + @23-27 RequiredValue( + @23-24 "y", + [], + @26-27 Num( + "0", + ), + ), + @29-34 IgnoredValue( + @29-31 "z", + [], + @33-34 Num( + "3", + ), + ), + @36-40 SpaceAfter( + IgnoredValue( + @36-37 "", + [], + @39-40 Num( + "2", + ), + ), + [ + Newline, + ], + ), + ], + }, + [ + Newline, + ], +) diff --git a/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.roc b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.roc new file mode 100644 index 00000000000..f42ce745668 --- /dev/null +++ b/crates/compiler/test_syntax/tests/snapshots/pass/record_builder_ignored_fields.expr.roc @@ -0,0 +1,2 @@ +{ Foo.Bar.baz <- x: 5, y: 0, _z: 3, _: 2 +} diff --git a/crates/compiler/test_syntax/tests/test_snapshots.rs b/crates/compiler/test_syntax/tests/test_snapshots.rs index 27736ae7a61..44fb528d4cb 100644 --- a/crates/compiler/test_syntax/tests/test_snapshots.rs +++ b/crates/compiler/test_syntax/tests/test_snapshots.rs @@ -449,6 +449,8 @@ mod test_snapshots { pass/qualified_field.expr, pass/qualified_var.expr, pass/record_access_after_tuple.expr, + pass/record_builder.expr, + pass/record_builder_ignored_fields.expr, pass/record_destructure_def.expr, pass/record_func_type_decl.expr, pass/record_type_with_function.expr, diff --git a/crates/language_server/src/analysis/tokens.rs b/crates/language_server/src/analysis/tokens.rs index 982d67c612a..f297e0e5788 100644 --- a/crates/language_server/src/analysis/tokens.rs +++ b/crates/language_server/src/analysis/tokens.rs @@ -446,7 +446,8 @@ where fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { match self { AssignedField::RequiredValue(field, _, ty) - | AssignedField::OptionalValue(field, _, ty) => (field_token(field.region, arena) + | AssignedField::OptionalValue(field, _, ty) + | AssignedField::IgnoredValue(field, _, ty) => (field_token(field.region, arena) .into_iter()) .chain(ty.iter_tokens(arena)) .collect_in(arena), diff --git a/crates/reporting/src/error/parse.rs b/crates/reporting/src/error/parse.rs index c37482cfb67..e801da8aabe 100644 --- a/crates/reporting/src/error/parse.rs +++ b/crates/reporting/src/error/parse.rs @@ -545,7 +545,7 @@ fn to_expr_report<'a>( to_record_report(alloc, lines, filename, erecord, *pos, start) } - EExpr::OptionalValueInRecordBuilder(region) => { + EExpr::OptionalValueInOldRecordBuilder(region) => { let surroundings = Region::new(start, region.end()); let region = lines.convert_region(*region); @@ -565,7 +565,7 @@ fn to_expr_report<'a>( } } - EExpr::RecordUpdateAccumulator(region) => { + EExpr::RecordUpdateOldBuilderField(region) => { let surroundings = Region::new(start, region.end()); let region = lines.convert_region(*region); @@ -1580,6 +1580,25 @@ fn to_import_report<'a>( severity, } } + Params(EImportParams::RecordIgnoredFieldFound(region), _) => { + let surroundings = Region::new(start, region.end()); + let region = lines.convert_region(*region); + + let doc = alloc.stack([ + alloc.reflow("I was partway through parsing module params, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region, severity), + alloc.reflow( + "This is an ignored record field, but those are not allowed in module params.", + ), + ]); + + Report { + filename, + doc, + title: "IGNORED RECORD FIELD IN MODULE PARAMS".to_string(), + severity, + } + } Params(EImportParams::RecordUpdateFound(region), _) => { let surroundings = Region::new(start, region.end()); let region = lines.convert_region(*region);