From 34b519eca29b329c99f58b0e9a919cda54623bd3 Mon Sep 17 00:00:00 2001 From: Sam Mohr Date: Tue, 21 Jan 2025 22:27:35 -0800 Subject: [PATCH] More work on record spread destructs --- crates/compiler/builtins/roc/Dict.roc | 22 +-- crates/compiler/can/src/copy.rs | 2 - crates/compiler/can/src/debug/pretty_print.rs | 13 +- crates/compiler/can/src/def.rs | 12 +- crates/compiler/can/src/exhaustive.rs | 50 +++++-- crates/compiler/can/src/expr.rs | 2 +- crates/compiler/can/src/module.rs | 19 ++- crates/compiler/can/src/pattern.rs | 35 +++-- crates/compiler/can/src/traverse.rs | 24 +++- crates/compiler/can/tests/test_can.rs | 3 - crates/compiler/constrain/src/module.rs | 6 +- crates/compiler/constrain/src/pattern.rs | 87 ++++++++++-- crates/compiler/exhaustive/src/lib.rs | 7 +- crates/compiler/fmt/src/pattern.rs | 83 ++++++++++- crates/compiler/load/tests/test_reporting.rs | 22 ++- crates/compiler/lower_params/src/lower.rs | 1 - crates/compiler/mono/src/ir/pattern.rs | 2 +- crates/compiler/parse/src/ast.rs | 130 ++++++++++-------- crates/compiler/parse/src/expr.rs | 9 +- crates/compiler/parse/src/normalize.rs | 1 + crates/compiler/parse/src/parser.rs | 2 + crates/compiler/parse/src/pattern.rs | 30 +++- crates/compiler/problem/src/can.rs | 8 +- crates/compiler/types/src/types.rs | 3 + crates/docs/src/lib.rs | 14 +- .../src/analysis/completion/visitor.rs | 8 +- crates/language_server/src/analysis/tokens.rs | 80 ++++++++--- crates/repl_eval/src/eval.rs | 30 +++- crates/reporting/src/error/canonicalize.rs | 8 +- crates/reporting/src/error/parse.rs | 24 ++-- crates/reporting/src/error/type.rs | 43 +++++- 31 files changed, 599 insertions(+), 181 deletions(-) diff --git a/crates/compiler/builtins/roc/Dict.roc b/crates/compiler/builtins/roc/Dict.roc index 01da8e64bdf..3522ea2fa20 100644 --- a/crates/compiler/builtins/roc/Dict.roc +++ b/crates/compiler/builtins/roc/Dict.roc @@ -218,7 +218,7 @@ release_excess_capacity = |@Dict({ buckets, data, max_bucket_capacity: original_ ## capacity_of_dict = Dict.capacity(food_dict) ## ``` capacity : Dict * * -> U64 -capacity = |@Dict({ max_bucket_capacity })| +capacity = |@Dict({ max_bucket_capacity, .. })| max_bucket_capacity ## Returns a dictionary containing the key and value provided as input. @@ -261,7 +261,7 @@ from_list = |data| ## |> Bool.is_eq(3) ## ``` len : Dict * * -> U64 -len = |@Dict({ data })| +len = |@Dict({ data, .. })| List.len(data) ## Check if the dictionary is empty. @@ -271,7 +271,7 @@ len = |@Dict({ data })| ## Dict.is_empty(Dict.empty({})) ## ``` is_empty : Dict * * -> Bool -is_empty = |@Dict({ data })| +is_empty = |@Dict({ data, .. })| List.is_empty(data) ## Clears all elements from a dictionary keeping around the allocation if it isn't huge. @@ -341,7 +341,7 @@ join_map = |dict, transform| ## |> Bool.is_eq(36) ## ``` walk : Dict k v, state, (state, k, v -> state) -> state -walk = |@Dict({ data }), initial_state, transform| +walk = |@Dict({ data, .. }), initial_state, transform| List.walk(data, initial_state, |state, (k, v)| transform(state, k, v)) ## Same as [Dict.walk], except you can stop walking early. @@ -373,7 +373,7 @@ walk = |@Dict({ data }), initial_state, transform| ## expect someone_is_an_adult == Bool.true ## ``` walk_until : Dict k v, state, (state, k, v -> [Continue state, Break state]) -> state -walk_until = |@Dict({ data }), initial_state, transform| +walk_until = |@Dict({ data, .. }), initial_state, transform| List.walk_until(data, initial_state, |state, (k, v)| transform(state, k, v)) ## Run the given function on each key-value pair of a dictionary, and return @@ -604,7 +604,7 @@ circular_dist = |start, end, size| ## |> Bool.is_eq([(1, "One"), (2, "Two"), (3, "Three"), (4, "Four")]) ## ``` to_list : Dict k v -> List (k, v) -to_list = |@Dict({ data })| +to_list = |@Dict({ data, .. })| data ## Returns the keys of a dictionary as a [List]. @@ -619,7 +619,7 @@ to_list = |@Dict({ data })| ## |> Bool.is_eq([1,2,3,4]) ## ``` keys : Dict k v -> List k -keys = |@Dict({ data })| +keys = |@Dict({ data, .. })| List.map(data, |(k, _)| k) ## Returns the values of a dictionary as a [List]. @@ -634,7 +634,7 @@ keys = |@Dict({ data })| ## |> Bool.is_eq(["One","Two","Three","Four"]) ## ``` values : Dict k v -> List v -values = |@Dict({ data })| +values = |@Dict({ data, .. })| List.map(data, |(_, v)| v) ## Combine two dictionaries by keeping the [union](https://en.wikipedia.org/wiki/Union_(set_theory)) @@ -757,7 +757,7 @@ decrement_dist = |dist_and_fingerprint| Num.sub_wrap(dist_and_fingerprint, dist_inc) find : Dict k v, k -> { bucket_index : U64, result : Result v [KeyNotFound] } -find = |@Dict({ buckets, data, shifts }), key| +find = |@Dict({ buckets, data, shifts, .. }), key| hash = hash_key(key) dist_and_fingerprint = dist_and_fingerprint_from_hash(hash) bucket_index = bucket_index_from_hash(hash, shifts) @@ -872,7 +872,7 @@ remove_bucket_helper = |buckets, bucket_index| (buckets, bucket_index) increase_size : Dict k v -> Dict k v -increase_size = |@Dict({ data, max_bucket_capacity, max_load_factor, shifts })| +increase_size = |@Dict({ data, max_bucket_capacity, max_load_factor, shifts, .. })| if max_bucket_capacity != max_bucket_count then new_shifts = shifts |> Num.sub_wrap(1) (buckets0, new_max_bucket_capacity) = alloc_buckets_from_shift(new_shifts, max_load_factor) @@ -1329,7 +1329,7 @@ init_seed = |seed| |> wymix(wyp1) |> Num.bitwise_xor(seed) -complete = |@LowLevelHasher({ state })| state +complete = |@LowLevelHasher({ state, .. })| state # These implementations hash each value individually with the seed and then mix # the resulting hash with the state. There are other options that may be faster diff --git a/crates/compiler/can/src/copy.rs b/crates/compiler/can/src/copy.rs index 3e5a86a0fb3..c96c8c3cdd5 100644 --- a/crates/compiler/can/src/copy.rs +++ b/crates/compiler/can/src/copy.rs @@ -799,12 +799,10 @@ fn deep_copy_pattern_help( } RecordDestructure { whole_var, - ext_var, destructs, opt_spread, } => RecordDestructure { whole_var: sub!(*whole_var), - ext_var: sub!(*ext_var), destructs: destructs .iter() .map(|lrd| { diff --git a/crates/compiler/can/src/debug/pretty_print.rs b/crates/compiler/can/src/debug/pretty_print.rs index 5e21e5b52f0..53997214460 100644 --- a/crates/compiler/can/src/debug/pretty_print.rs +++ b/crates/compiler/can/src/debug/pretty_print.rs @@ -538,7 +538,11 @@ fn pattern<'a>( } => text!(f, "@{} ", opaque.module_string(c.interns)) .append(pattern(c, Free, f, &argument.1.value)) .group(), - RecordDestructure { destructs, .. } => f + RecordDestructure { + destructs, + opt_spread, + whole_var: _, + } => f .text("{") .append( f.intersperse( @@ -558,6 +562,13 @@ fn pattern<'a>( f.text(", "), ), ) + .append(match &**opt_spread { + None => f.text(""), + Some(spread) => match &spread.opt_pattern.value { + None => f.text(".."), + Some(spread_pat) => f.text("..").append(pattern(c, Free, f, &spread_pat.value)), + }, + }) .append(f.text("}")) .group(), TupleDestructure { destructs, .. } => f diff --git a/crates/compiler/can/src/def.rs b/crates/compiler/can/src/def.rs index eadce73347f..afb91d32de6 100644 --- a/crates/compiler/can/src/def.rs +++ b/crates/compiler/can/src/def.rs @@ -2181,10 +2181,20 @@ fn pattern_to_vars_by_symbol( } } - RecordDestructure { destructs, .. } => { + RecordDestructure { + destructs, + opt_spread, + whole_var: _, + } => { for destruct in destructs { vars_by_symbol.insert(destruct.value.symbol, destruct.value.var); } + + if let Some(spread) = &**opt_spread { + if let Some(spread_pat) = &spread.opt_pattern.value { + pattern_to_vars_by_symbol(vars_by_symbol, &spread_pat.value, spread.spread_var); + } + } } List { diff --git a/crates/compiler/can/src/exhaustive.rs b/crates/compiler/can/src/exhaustive.rs index 621dc407d5a..7dd706dbcbf 100644 --- a/crates/compiler/can/src/exhaustive.rs +++ b/crates/compiler/can/src/exhaustive.rs @@ -79,8 +79,13 @@ enum SketchedPattern { enum IndexCtor<'a> { /// Index an opaque type. There should be one argument. Opaque, - /// Index a record type. The arguments are the types of the record fields. - Record(&'a [Lowercase]), + // TODO: can we get this size down to a pointer width or less to avoid bloating the size? + /// Index a record type. + Record { + /// The arguments are the types of the record fields. + fields: &'a [Lowercase], + opt_spread: Option<&'a IndexCtor<'a>>, + }, /// Index a tuple type. Tuple, /// Index a guard constructor. The arguments are a faux guard pattern, and then the real @@ -114,7 +119,7 @@ impl<'a> IndexCtor<'a> { Self::Tag(tag_name) } RenderAs::Opaque => Self::Opaque, - RenderAs::Record(fields) => Self::Record(fields), + RenderAs::Record { fields, opt_spread } => Self::Record { fields, opt_spread }, RenderAs::Tuple => Self::Tuple, RenderAs::Guard => Self::Guard, } @@ -165,7 +170,7 @@ fn index_var( FlatType::Apply(..) => internal_error!("not an indexable constructor"), FlatType::Record(fields, ext) => { let fields_order = match render_as { - RenderAs::Record(fields) => fields, + RenderAs::Record { fields, opt_spread } => fields, _ => internal_error!( "record constructors must always be rendered as records" ), @@ -228,12 +233,12 @@ fn index_var( return Ok(vec![]); } FlatType::EmptyRecord => { - debug_assert!(matches!(ctor, IndexCtor::Record(..))); + debug_assert!(matches!(ctor, IndexCtor::Record { .. })); // If there are optional record fields we don't unify them, but we need to // cover them. Since optional fields correspond to "any" patterns, we can pass // through arbitrary types. let num_fields = match render_as { - RenderAs::Record(fields) => fields.len(), + RenderAs::Record { fields, opt_spread } => fields.len(), _ => internal_error!( "record constructors must always be rendered as records" ), @@ -338,9 +343,20 @@ fn sketch_pattern(pattern: &crate::pattern::Pattern) -> SketchedPattern { &FloatLiteral(_, _, _, f, _) => SP::Literal(Literal::Float(f64::to_bits(f))), StrLiteral(v) => SP::Literal(Literal::Str(v.clone())), &SingleQuote(_, _, c, _) => SP::Literal(Literal::Byte(c as u8)), - RecordDestructure { destructs, .. } => { + RecordDestructure { + destructs, + opt_spread, + whole_var: _, + } => { let tag_id = TagId(0); - let mut patterns = std::vec::Vec::with_capacity(destructs.len()); + let mut patterns = std::vec::Vec::with_capacity( + destructs.len() + + if opt_spread.is_some_and(|spread| spread.opt_pattern.value.is_some()) { + 1 + } else { + 0 + }, + ); let mut field_names = std::vec::Vec::with_capacity(destructs.len()); for Loc { @@ -358,8 +374,24 @@ fn sketch_pattern(pattern: &crate::pattern::Pattern) -> SketchedPattern { } } + let spread_render_as = if let Some(spread) = &**opt_spread { + match &spread.opt_pattern.value { + None => Box::new(Some(None)), + Some(spread_pat) => { + let inner_sp = sketch_pattern(&spread_pat.value); + patterns.push(inner_sp); + Box::new(Some(Some(RenderAs))) + } + } + } else { + Box::new(None) + }; + let union = Union { - render_as: RenderAs::Record(field_names), + render_as: RenderAs::Record { + fields: field_names, + opt_spread: spread_render_as, + }, alternatives: vec![Ctor { name: CtorName::Tag(TagName("#Record".into())), tag_id, diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs index 0f1bafa7342..72b034a2530 100644 --- a/crates/compiler/can/src/expr.rs +++ b/crates/compiler/can/src/expr.rs @@ -1547,7 +1547,7 @@ pub fn canonicalize_expr<'a>( loc_binop2, ); - InvalidPrecedence(problem, region) + InvalidPrecedence(problem) } }; diff --git a/crates/compiler/can/src/module.rs b/crates/compiler/can/src/module.rs index fd2fedfe176..49dd47f6bd3 100644 --- a/crates/compiler/can/src/module.rs +++ b/crates/compiler/can/src/module.rs @@ -142,7 +142,6 @@ pub struct ModuleParams { pub whole_symbol: Symbol, pub whole_var: Variable, pub record_var: Variable, - pub record_ext_var: Variable, pub destructs: Vec>, // used while lowering passed functions pub arity_by_name: VecMap, @@ -152,7 +151,6 @@ impl ModuleParams { pub fn pattern(&self) -> Loc { let record_pattern = Pattern::RecordDestructure { whole_var: self.record_var, - ext_var: self.record_ext_var, destructs: self.destructs.clone(), opt_spread: Box::new(None), }; @@ -310,7 +308,6 @@ pub fn canonicalize_module_defs<'a>( whole_var, whole_symbol, record_var: var_store.fresh(), - record_ext_var: var_store.fresh(), destructs, arity_by_name: Default::default(), } @@ -823,7 +820,11 @@ fn fix_values_captured_in_closure_pattern( closure_captures, ); } - RecordDestructure { destructs, .. } => { + RecordDestructure { + destructs, + whole_var: _, + opt_spread, + } => { for loc_destruct in destructs.iter_mut() { use crate::pattern::DestructType::*; match &mut loc_destruct.value.typ { @@ -840,6 +841,16 @@ fn fix_values_captured_in_closure_pattern( ), } } + + if let Some(spread) = &mut **opt_spread { + if let Some(spread_pat) = &mut spread.opt_pattern.value { + fix_values_captured_in_closure_pattern( + &mut spread_pat.value, + no_capture_symbols, + closure_captures, + ); + } + } } TupleDestructure { destructs, .. } => { for loc_destruct in destructs.iter_mut() { diff --git a/crates/compiler/can/src/pattern.rs b/crates/compiler/can/src/pattern.rs index 1c8ca32da31..be1601b8e7a 100644 --- a/crates/compiler/can/src/pattern.rs +++ b/crates/compiler/can/src/pattern.rs @@ -62,7 +62,6 @@ pub enum Pattern { }, RecordDestructure { whole_var: Variable, - ext_var: Variable, destructs: Vec>, opt_spread: Box>, }, @@ -145,12 +144,26 @@ impl Pattern { | MalformedPattern(..) | AbilityMemberSpecialization { .. } => true, - RecordDestructure { destructs, .. } => { + RecordDestructure { + destructs, + opt_spread, + whole_var: _, + } => { // If all destructs are surely exhaustive, then this is surely exhaustive. - destructs.iter().all(|d| match &d.value.typ { + if !destructs.iter().all(|d| match &d.value.typ { DestructType::Required | DestructType::Optional(_, _) => true, DestructType::Guard(_, pat) => pat.value.surely_exhaustive(), - }) + }) { + return false; + } + + match &**opt_spread { + Some(spread) => match &spread.opt_pattern.value { + Some(spread_pat) => spread_pat.value.surely_exhaustive(), + None => true, + }, + None => true, + } } TupleDestructure { destructs, .. } => { // If all destructs are surely exhaustive, then this is surely exhaustive. @@ -679,7 +692,6 @@ pub fn canonicalize_pattern<'a>( } RecordDestructure(patterns) => { - let ext_var = var_store.fresh(); let whole_var = var_store.fresh(); let (destructs, opt_spread, opt_erroneous) = canonicalize_record_destructs( @@ -697,7 +709,6 @@ pub fn canonicalize_pattern<'a>( // use the resulting RuntimeError. Otherwise, return a successful record destructure. opt_erroneous.unwrap_or(Pattern::RecordDestructure { whole_var, - ext_var, destructs, opt_spread: Box::new(opt_spread), }) @@ -1100,8 +1111,16 @@ impl<'a> BindingsFromPattern<'a> { let it = destructs.iter().rev().map(TupleDestruct); stack.extend(it); } - RecordDestructure { destructs, .. } => { - let it = destructs.iter().rev().map(RecordDestruct); + RecordDestructure { + destructs, + opt_spread, + whole_var: _, + } => { + let it = destructs.iter().rev().map(RecordDestruct).chain( + opt_spread.iter().flat_map(|spread| { + spread.opt_pattern.value.as_ref().map(Pattern) + }), + ); stack.extend(it); } NumLiteral(..) diff --git a/crates/compiler/can/src/traverse.rs b/crates/compiler/can/src/traverse.rs index f01e8ed2cbd..087abb9514f 100644 --- a/crates/compiler/can/src/traverse.rs +++ b/crates/compiler/can/src/traverse.rs @@ -11,7 +11,7 @@ use crate::{ self, AnnotatedMark, ClosureData, Declarations, Expr, Field, OpaqueWrapFunctionData, StructAccessorData, }, - pattern::{DestructType, Pattern, RecordDestruct, TupleDestruct}, + pattern::{DestructType, Pattern, RecordDestruct, RecordDestructureSpread, TupleDestruct}, }; #[derive(Clone)] pub enum DeclarationInfo<'a> { @@ -574,6 +574,12 @@ pub trait Visitor: Sized { } } + fn visit_record_spread_destruct(&mut self, destruct: &RecordDestructureSpread) { + if let Some(spread_pat) = &destruct.opt_pattern.value { + walk_pattern(self, &spread_pat.value); + } + } + fn visit_tuple_destruct(&mut self, destruct: &TupleDestruct, region: Region) { if self.should_visit(region) { self.visit_pattern( @@ -600,9 +606,19 @@ pub fn walk_pattern(visitor: &mut V, pattern: &Pattern) { let (v, lp) = &**argument; visitor.visit_pattern(&lp.value, lp.region, Some(*v)); } - RecordDestructure { destructs, .. } => destructs - .iter() - .for_each(|d| visitor.visit_record_destruct(&d.value, d.region)), + RecordDestructure { + destructs, + opt_spread, + whole_var: _, + } => { + destructs + .iter() + .for_each(|d| visitor.visit_record_destruct(&d.value, d.region)); + + if let Some(spread) = &**opt_spread { + visitor.visit_record_spread_destruct(spread); + } + } TupleDestructure { destructs, .. } => destructs .iter() .for_each(|d| visitor.visit_tuple_destruct(&d.value, d.region)), diff --git a/crates/compiler/can/tests/test_can.rs b/crates/compiler/can/tests/test_can.rs index 542e3c2b8ef..2f1bdf0caca 100644 --- a/crates/compiler/can/tests/test_can.rs +++ b/crates/compiler/can/tests/test_can.rs @@ -642,9 +642,6 @@ mod test_can { } = can_expr_with(&arena, test_home(), src); assert_eq!(problems.len(), 1); - assert!(problems - .iter() - .all(|problem| matches!(problem, Problem::InvalidOptionalValue { .. }))); assert!(matches!( loc_expr.value, diff --git a/crates/compiler/constrain/src/module.rs b/crates/compiler/constrain/src/module.rs index 470039d4c84..ba297a76ea2 100644 --- a/crates/compiler/constrain/src/module.rs +++ b/crates/compiler/constrain/src/module.rs @@ -64,10 +64,10 @@ fn constrain_params( let mut state = PatternState::default(); - let empty_rec = constraints.push_type(types, Types::EMPTY_RECORD); - let closed_con = constraints.store(empty_rec, module_params.record_ext_var, file!(), line!()); + // let empty_rec = constraints.push_type(types, Types::EMPTY_RECORD); + // let closed_con = constraints.store(empty_rec, Variable::EMPTY_RECORD, file!(), line!()); - state.constraints.push(closed_con); + // state.constraints.push(closed_con); constrain_pattern( types, diff --git a/crates/compiler/constrain/src/pattern.rs b/crates/compiler/constrain/src/pattern.rs index c9cb78da375..5207991850c 100644 --- a/crates/compiler/constrain/src/pattern.rs +++ b/crates/compiler/constrain/src/pattern.rs @@ -11,8 +11,8 @@ use roc_module::symbol::Symbol; use roc_region::all::{Loc, Region}; use roc_types::subs::Variable; use roc_types::types::{ - AliasKind, AliasShared, Category, OptAbleType, PReason, PatternCategory, Reason, RecordField, - Type, TypeExtension, TypeTag, Types, + AliasKind, AliasShared, Category, ExtImplicitOpenness, OptAbleType, PReason, PatternCategory, + Reason, RecordField, Type, TypeExtension, TypeTag, Types, }; use soa::Index; @@ -91,13 +91,19 @@ fn headers_from_annotation_help( | SingleQuote(..) | StrLiteral(_) => true, - RecordDestructure { destructs, .. } => { + RecordDestructure { + whole_var: _, + destructs, + opt_spread, + } => { let dealiased = types.shallow_dealias(annotation.value); match types[dealiased] { TypeTag::Record(fields) => { let (field_names, _, field_types) = types.record_fields_slices(fields); let field_names = &types[field_names]; + let ext_slice = types.get_type_arguments(annotation.value); + for loc_destruct in destructs { let destruct = &loc_destruct.value; @@ -124,7 +130,25 @@ fn headers_from_annotation_help( return false; } } - true + + match &**opt_spread { + None => true, + Some(spread_destruct) => { + match (&spread_destruct.opt_pattern.value, ext_slice.is_empty()) { + (Some(pat), false) => { + let spread_type = ext_slice.at(0); + headers_from_annotation_help( + types, + constraints, + &pat.value, + &Loc::at(annotation.region, spread_type), + headers, + ) + } + _ => false, + } + } + } } TypeTag::EmptyRecord => destructs.is_empty(), _ => false, @@ -593,13 +617,10 @@ pub fn constrain_pattern_help( RecordDestructure { whole_var, - ext_var, destructs, opt_spread, } => { state.vars.push(*whole_var); - state.vars.push(*ext_var); - let ext_type = Type::Variable(*ext_var); let mut field_types: SendMap> = SendMap::default(); @@ -713,17 +734,61 @@ pub fn constrain_pattern_help( state.vars.push(*var); } + let extension_type = match &**opt_spread { + None => TypeExtension::Closed, + Some(spread) => match &spread.opt_pattern.value { + None => TypeExtension::from_type( + Type::Variable(spread.spread_var), + ExtImplicitOpenness::Yes, + ), + Some(spread_pattern) => { + let spread_type = Type::Variable(spread.spread_var); + let spread_type_index = constraints.push_variable(spread.spread_var); + + let expected_pat = + constraints.push_pat_expected_type(PExpected::ForReason( + PReason::SpreadGuard, + spread_type_index, + spread_pattern.region, + )); + + state.constraints.push(constraints.pattern_presence( + spread_type_index, + expected_pat, + PatternCategory::CapturingSpread, + region, + )); + state.vars.push(spread.spread_var); + + constrain_pattern_help( + types, + constraints, + env, + &spread_pattern.value, + spread_pattern.region, + expected, + state, + ); + + TypeExtension::from_non_annotation_type(spread_type) + } + }, + }; + + let record_typ = if field_types.is_empty() && opt_spread.is_none() { + Type::EmptyRec + } else { + Type::Record(field_types, extension_type) + }; let record_type = { - let typ = types.from_old_type(&Type::Record( - field_types, - TypeExtension::from_non_annotation_type(ext_type), - )); + let typ = types.from_old_type(&record_typ); constraints.push_type(types, typ) }; let whole_var_index = constraints.push_variable(*whole_var); let expected_record = constraints.push_expected_type(Expected::NoExpectation(record_type)); + let whole_con = constraints.equal_types( whole_var_index, expected_record, diff --git a/crates/compiler/exhaustive/src/lib.rs b/crates/compiler/exhaustive/src/lib.rs index c4b36af2147..f1ee8d1ee03 100644 --- a/crates/compiler/exhaustive/src/lib.rs +++ b/crates/compiler/exhaustive/src/lib.rs @@ -37,7 +37,12 @@ impl Union { pub enum RenderAs { Tag, Opaque, - Record(Vec), + Record { + fields: Vec, + /// The first option is whether there is a spread, + /// the second is whether the spread has a subpattern + opt_spread: Box>>, + }, Tuple, Guard, } diff --git a/crates/compiler/fmt/src/pattern.rs b/crates/compiler/fmt/src/pattern.rs index 968b87d9e41..12f717dd971 100644 --- a/crates/compiler/fmt/src/pattern.rs +++ b/crates/compiler/fmt/src/pattern.rs @@ -61,13 +61,82 @@ impl<'a> Formattable for RecordFieldPattern<'a> { } } - fn format_with_options( - &self, - buf: &mut Buf, - _parens: Parens, - _newlines: Newlines, - indent: u16, - ) { + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + let is_multiline = newlines == Newlines::Yes; + + match self { + RecordFieldPattern::RequiredField { label, inner } => { + if is_multiline { + buf.newline(); + } + + buf.indent(indent); + + if buf.flags().snakify { + snakify_camel_ident(buf, label); + } else { + buf.push_str(label); + } + + buf.push_str_allow_spaces(" : "); + + inner.value.format(buf, indent); + } + RecordFieldPattern::Identifier { label } => { + if is_multiline { + buf.newline(); + } + + buf.indent(indent); + + if buf.flags().snakify { + snakify_camel_ident(buf, label); + } else { + buf.push_str(label); + } + } + RecordFieldPattern::OptionalField { + label, + default_value, + } => { + if is_multiline { + buf.newline(); + } + + buf.indent(indent); + + if buf.flags().snakify { + snakify_camel_ident(buf, label); + } else { + buf.push_str(label); + } + + buf.push_str_allow_spaces(" ?? "); + + default_value.value.format(buf, indent); + } + RecordFieldPattern::Spread { opt_pattern } => { + if is_multiline { + buf.newline(); + } + + buf.indent(indent); + + buf.push_str(".."); + + if let Some(spread) = opt_pattern { + spread.value.format(buf, indent); + } + } + RecordFieldPattern::SpaceBefore(sub_field, spaces) => { + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + sub_field.format_with_options(buf, parens, newlines, indent); + } + RecordFieldPattern::SpaceAfter(sub_field, spaces) => { + sub_field.format_with_options(buf, parens, newlines, indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + } } } diff --git a/crates/compiler/load/tests/test_reporting.rs b/crates/compiler/load/tests/test_reporting.rs index 73018a84a8e..e33f647b360 100644 --- a/crates/compiler/load/tests/test_reporting.rs +++ b/crates/compiler/load/tests/test_reporting.rs @@ -596,7 +596,7 @@ mod test_reporting { " ), @r" - ── SYNTAX PROBLEM in /code/proj/Main.roc ─────────────────────────────────────── + ── INVALID PRECEDENCE in /code/proj/Main.roc ─────────────────────────────────── Using != and == together requires parentheses, to clarify how they should be grouped. @@ -681,7 +681,7 @@ mod test_reporting { " ), @r" - ── SYNTAX PROBLEM in /code/proj/Main.roc ─────────────────────────────────────── + ── INVALID PRECEDENCE in /code/proj/Main.roc ─────────────────────────────────── Using more than one == like this requires parentheses, to clarify how things should be grouped. @@ -8402,9 +8402,9 @@ All branches in an `if` must have the same type! 9│ @F {} -> "" ^^^^^ - The 2nd pattern is trying to matchF unwrappings of type: + The 2nd pattern is trying to match F unwrappings of type: - F {}a + F {} But all the previous branches match: @@ -9950,8 +9950,6 @@ All branches in an `if` must have the same type! Custom implementations must be supplied fully. - - ── INCOMPLETE ABILITY IMPLEMENTATION in /code/proj/Main.roc ──────────────────── This type does not fully implement the `MEq` ability: @@ -10849,7 +10847,7 @@ All branches in an `if` must have the same type! } "# ), - @r#" + @r" ── OPTIONAL FIELD IN RECORD BUILDER in /code/proj/Main.roc ───────────────────── Optional fields are not allowed to be used in record builders. @@ -10859,8 +10857,8 @@ All branches in an `if` must have the same type! 6│> c? 456 7│ } - Record builders can only have required values for their fields. - "# + Record builders can only have defined values for their fields. + " ); // CalledVia::RecordBuilder => { @@ -12109,7 +12107,7 @@ All branches in an `if` must have the same type! foo : a -> {} where a implements Hash - main = foo ("", \{} -> {}) + main = foo ("", \{ .. } -> {}) "# ), @r#" @@ -12117,8 +12115,8 @@ All branches in an `if` must have the same type! This expression has a type that does not implement the abilities it's expected to: - 5│ main = foo ("", \{} -> {}) - ^^^^^^^^^^^^^^^ + 5│ main = foo ("", \{ .. } -> {}) + ^^^^^^^^^^^^^^^^^^^ I can't generate an implementation of the `Hash` ability for diff --git a/crates/compiler/lower_params/src/lower.rs b/crates/compiler/lower_params/src/lower.rs index 1ecd7f1a070..4222d6bc131 100644 --- a/crates/compiler/lower_params/src/lower.rs +++ b/crates/compiler/lower_params/src/lower.rs @@ -434,7 +434,6 @@ impl<'a> LowerParams<'a> { let record_pattern = Pattern::RecordDestructure { whole_var: module_params.record_var, - ext_var: module_params.record_ext_var, destructs, opt_spread: Box::new(None), }; diff --git a/crates/compiler/mono/src/ir/pattern.rs b/crates/compiler/mono/src/ir/pattern.rs index c9089d7728e..95261e163e3 100644 --- a/crates/compiler/mono/src/ir/pattern.rs +++ b/crates/compiler/mono/src/ir/pattern.rs @@ -936,7 +936,7 @@ fn from_can_pattern_help<'a>( RecordDestructure { whole_var, destructs, - .. + opt_spread, } => { // sorted fields based on the type let sorted_fields = { diff --git a/crates/compiler/parse/src/ast.rs b/crates/compiler/parse/src/ast.rs index cdd2e92cf51..cac52c6dfb7 100644 --- a/crates/compiler/parse/src/ast.rs +++ b/crates/compiler/parse/src/ast.rs @@ -1656,65 +1656,85 @@ pub enum RecordFieldPattern<'a> { } impl<'a> RecordFieldPattern<'a> { - // TODO: check for equivalence based on whether there are any spreads, zip is a footgun here! - pub fn fields_are_equivalent( - lefts: &[Loc], - rights: &[Loc], - // arena: &'a Bump, - ) -> bool { - // let mut left_required_fields = Vec::with_capacity_in(lefts.len(), arena); - // let mut left_optional_fields = Vec::with_capacity_in(lefts.len(), arena); - - // let mut right_required_fields = Vec::with_capacity_in(rights.len(), arena); - // let mut right_optional_fields = Vec::with_capacity_in(rights.len(), arena); + pub fn fields_are_equivalent(lefts: &[Loc], rights: &[Loc]) -> bool { + if lefts.len() != rights.len() { + return false; + } + + for (left, right) in lefts.iter().zip(rights.iter()) { + if !left.value.equivalent(&right.value) { + return false; + } + } true } - // pub fn equivalent(&self, other: &Self) -> bool { - // match self { - // Self::RequiredField { label, value } => match other { - // Self::RequiredField { - // label: other_label, - // value, - // } => label == other_label, - // Self::OptionalField { - // label: other_label, - // default_value, - // } => {} - // Self::Identifier { label: other_label } => label == other_label, - // Self::SpaceBefore(inner, _) | Self::SpaceAfter(inner, _) => inner.equivalent(other), - // }, - // Self::OptionalField { - // label, - // default_value, - // } => match other { - // Self::RequiredField { - // label: other_label, - // value, - // } => label == other_label, - // Self::OptionalField { - // label, - // default_value, - // } => {} - // Self::Identifier { label: other_label } => label == other_label, - // Self::SpaceBefore(inner, _) | Self::SpaceAfter(inner, _) => inner.equivalent(other), - // }, - // Self::Identifier { label } => match other { - // Self::RequiredField { - // label: other_label, - // value, - // } => label == other_label, - // Self::OptionalField { - // label, - // default_value, - // } => {} - // Self::Identifier { label: other_label } => label == other_label, - // Self::SpaceBefore(inner, _) | Self::SpaceAfter(inner, _) => inner.equivalent(other), - // }, - // Self::SpaceBefore(inner, _) | Self::SpaceAfter(inner, _) => inner.equivalent(other), - // } - // } + fn equivalent(&self, other: &Self) -> bool { + match self { + Self::RequiredField { label, inner } => match other { + Self::RequiredField { + label: other_label, + inner: inner_other, + } => label == other_label && inner.value.equivalent(&inner_other.value), + Self::OptionalField { + label: _, + default_value: _, + } => false, + Self::Identifier { label: other_label } => label == other_label, + Self::Spread { opt_pattern: _ } => false, + Self::SpaceBefore(inner_other, _) | Self::SpaceAfter(inner_other, _) => { + self.equivalent(inner_other) + } + }, + Self::OptionalField { + label, + default_value: _, + } => match other { + Self::RequiredField { label: _, inner: _ } => false, + Self::OptionalField { + label: other_label, + default_value: _, + } => label == other_label, + Self::Identifier { label: other_label } => label == other_label, + Self::Spread { opt_pattern: _ } => false, + Self::SpaceBefore(inner_other, _) | Self::SpaceAfter(inner_other, _) => { + self.equivalent(inner_other) + } + }, + Self::Identifier { label } => match other { + Self::RequiredField { + label: other_label, + inner: _, + } => label == other_label, + Self::OptionalField { + label: other_label, + default_value: _, + } => label == other_label, + Self::Identifier { label: other_label } => label == other_label, + Self::Spread { opt_pattern: _ } => false, + Self::SpaceBefore(inner_other, _) | Self::SpaceAfter(inner_other, _) => { + self.equivalent(inner_other) + } + }, + Self::Spread { opt_pattern } => match other { + Self::Spread { + opt_pattern: other_opt_pattern, + } => match (opt_pattern, other_opt_pattern) { + (Some(pat), Some(other_pat)) => pat.value.equivalent(&other_pat.value), + (None, None) => true, + (Some(_), None) | (None, Some(_)) => false, + }, + Self::RequiredField { .. } + | Self::OptionalField { .. } + | Self::Identifier { .. } => false, + Self::SpaceBefore(inner_other, _) | Self::SpaceAfter(inner_other, _) => { + self.equivalent(inner_other) + } + }, + Self::SpaceBefore(inner, _) | Self::SpaceAfter(inner, _) => inner.equivalent(other), + } + } } impl<'a> Spaceable<'a> for RecordFieldPattern<'a> { diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs index cc6dda7023e..d4dc3107bbc 100644 --- a/crates/compiler/parse/src/expr.rs +++ b/crates/compiler/parse/src/expr.rs @@ -179,6 +179,7 @@ fn loc_term_or_underscore_or_conditional<'a>( loc_term_or_closure(check_for_arrow).parse(arena, state, min_indent) } } + fn loc_conditional<'a>( check_for_arrow: CheckForArrow, ) -> impl Parser<'a, Loc>, EExpr<'a>> { @@ -3775,17 +3776,17 @@ pub fn record_field<'a>() -> impl Parser<'a, RecordField<'a>, ERecord<'a>> { map( either( - and( + backtrackable(and( two_bytes(b'.', b'.', ERecord::DoubleDot), optional(specialize_err_ref(ERecord::SpreadExpr, loc_expr(true))), - ), + )), and( loc(and( optional(byte(b'_', ERecord::UnderscoreField)), specialize_err(|_, pos| ERecord::Field(pos), lowercase_ident()), )), optional(and( - and(spaces(), value_prefix), + and(backtrackable(spaces()), value_prefix), spaces_before(specialize_err_ref(ERecord::Expr, loc_expr(true))), )), ), @@ -3793,7 +3794,7 @@ pub fn record_field<'a>() -> impl Parser<'a, RecordField<'a>, ERecord<'a>> { |either_spread_or_field| match either_spread_or_field { Either::First((_spread, opt_value)) => SpreadValue(opt_value), Either::Second((loc_name_with_opt_underscore, opt_value_with_prefix)) => { - let ignored = loc_name_with_opt_underscore.value.0.is_none(); + let ignored = loc_name_with_opt_underscore.value.0.is_some(); let loc_name = Loc { region: loc_name_with_opt_underscore.region, value: loc_name_with_opt_underscore.value.1, diff --git a/crates/compiler/parse/src/normalize.rs b/crates/compiler/parse/src/normalize.rs index a66aafea10e..755dac11da5 100644 --- a/crates/compiler/parse/src/normalize.rs +++ b/crates/compiler/parse/src/normalize.rs @@ -1578,6 +1578,7 @@ impl<'a> Normalize<'a> for PRecord<'a> { PRecord::Colon(_) => PRecord::Colon(Position::zero()), PRecord::OptionalFirst(_) => PRecord::OptionalFirst(Position::zero()), PRecord::OptionalSecond(_) => PRecord::OptionalSecond(Position::zero()), + PRecord::Spread(_) => PRecord::Spread(Position::zero()), PRecord::Pattern(inner_err, _) => { PRecord::Pattern(arena.alloc(inner_err.normalize(arena)), Position::zero()) } diff --git a/crates/compiler/parse/src/parser.rs b/crates/compiler/parse/src/parser.rs index 488a5a4b786..8df3ef664e0 100644 --- a/crates/compiler/parse/src/parser.rs +++ b/crates/compiler/parse/src/parser.rs @@ -1117,6 +1117,7 @@ pub enum PRecord<'a> { Colon(Position), OptionalFirst(Position), OptionalSecond(Position), + Spread(Position), Pattern(&'a EPattern<'a>, Position), Expr(&'a EExpr<'a>, Position), @@ -1138,6 +1139,7 @@ impl<'a> PRecord<'a> { | PRecord::Colon(p) | PRecord::OptionalFirst(p) | PRecord::OptionalSecond(p) + | PRecord::Spread(p) | PRecord::Space(_, p) => Region::from_pos(*p), } } diff --git a/crates/compiler/parse/src/pattern.rs b/crates/compiler/parse/src/pattern.rs index b9a20d02e14..fe1e1d7f9ad 100644 --- a/crates/compiler/parse/src/pattern.rs +++ b/crates/compiler/parse/src/pattern.rs @@ -551,7 +551,35 @@ fn record_pattern_field<'a>() -> impl Parser<'a, Loc>, PR use crate::parser::Either::*; move |arena, state: State<'a>, min_indent: u32| { - // You must have a field name, e.g. "email" + let before_field_pos = state.pos(); + let (_, opt_spread_dots, state) = + optional(two_bytes(b'.', b'.', PRecord::Spread)).parse(arena, state, min_indent)?; + + if opt_spread_dots.is_some() { + let (_, opt_loc_pattern, state) = + optional(specialize_err_ref(PRecord::Pattern, loc_pattern_help())) + .parse(arena, state, min_indent)?; + + let spread_region = Region::new( + before_field_pos, + opt_loc_pattern + .map(|loc_pat| loc_pat.region.end()) + .unwrap_or_else(|| state.pos()), + ); + + return Ok(( + MadeProgress, + Loc::at( + spread_region, + RecordFieldPattern::Spread { + opt_pattern: opt_loc_pattern.map(|pat| &*arena.alloc(pat)), + }, + ), + state, + )); + } + + // If this isn't a spread, it must have a field name, e.g. "email" // using the initial pos is important for error reporting let pos = state.pos(); let (progress, loc_label, state) = loc(specialize_err( diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index 829503ab2f2..3006681fe79 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -662,7 +662,7 @@ pub enum RuntimeError { error: io::ErrorKind, region: Region, }, - InvalidPrecedence(PrecedenceProblem, Region), + InvalidPrecedence(PrecedenceProblem), MalformedIdentifier(Box, roc_parse::ident::BadIdent, Region), MalformedTypeName(Box, Region), InvalidRecordUpdate { @@ -742,7 +742,11 @@ impl RuntimeError { | RuntimeError::OpaqueAppliedToMultipleArgs(region) | RuntimeError::ValueNotExposed { region, .. } | RuntimeError::ModuleNotImported { region, .. } - | RuntimeError::InvalidPrecedence(_, region) + | RuntimeError::InvalidPrecedence(PrecedenceProblem::BothNonAssociative( + region, + _, + _, + )) | RuntimeError::MalformedIdentifier(_, _, region) | RuntimeError::MalformedTypeName(_, region) | RuntimeError::InvalidRecordUpdate { region } diff --git a/crates/compiler/types/src/types.rs b/crates/compiler/types/src/types.rs index 6f63a800199..8c8f7df691a 100644 --- a/crates/compiler/types/src/types.rs +++ b/crates/compiler/types/src/types.rs @@ -3392,6 +3392,7 @@ pub enum PReason { ListElem, PatternGuard, OptionalField, + SpreadGuard, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -3556,6 +3557,8 @@ pub enum PatternCategory { EmptyRecord, PatternGuard, PatternDefault, + NonCapturingSpread, + CapturingSpread, Set, Map, Ctor(TagName), diff --git a/crates/docs/src/lib.rs b/crates/docs/src/lib.rs index 12cc699057a..d080535d5c4 100644 --- a/crates/docs/src/lib.rs +++ b/crates/docs/src/lib.rs @@ -837,6 +837,15 @@ fn type_annotation_to_html( RecordField::RecordField { name, .. } => name, RecordField::OptionalField { name, .. } => name, RecordField::LabelOnly { name } => name, + RecordField::Spread { type_annotation } => { + buf.push_str(".."); + if let Some(type_ann) = type_annotation { + // TODO: is `need_parens` actually always false? + type_annotation_to_html(indent_level, buf, type_ann, false); + } + + continue; + } }; buf.push_str(fields_name.as_str()); @@ -854,7 +863,7 @@ fn type_annotation_to_html( buf.push_str(" ? "); type_annotation_to_html(next_indent_level, buf, type_annotation, false); } - RecordField::LabelOnly { .. } => {} + RecordField::LabelOnly { .. } | RecordField::Spread { .. } => {} } if is_multiline { @@ -1106,6 +1115,9 @@ fn should_be_multiline(type_ann: &TypeAnnotation) -> bool { type_annotation, .. } => should_be_multiline(type_annotation), RecordField::LabelOnly { .. } => false, + RecordField::Spread { type_annotation } => type_annotation + .as_ref() + .is_some_and(|type_ann| should_be_multiline(type_ann)), }) } TypeAnnotation::Ability { .. } => true, diff --git a/crates/language_server/src/analysis/completion/visitor.rs b/crates/language_server/src/analysis/completion/visitor.rs index ad0fb8905c5..5aae1953692 100644 --- a/crates/language_server/src/analysis/completion/visitor.rs +++ b/crates/language_server/src/analysis/completion/visitor.rs @@ -195,9 +195,11 @@ impl CompletionVisitor<'_> { roc_can::pattern::Pattern::As(pat, symbol) => { self.as_pattern(&pat.value, *symbol, pattern_var) } - roc_can::pattern::Pattern::RecordDestructure { destructs, .. } => { - self.record_destructure(destructs) - } + roc_can::pattern::Pattern::RecordDestructure { + destructs, + opt_spread, + whole_var: _, + } => self.record_destructure(destructs, opt_spread), roc_can::pattern::Pattern::TupleDestructure { destructs, .. } => { self.tuple_destructure(destructs) } diff --git a/crates/language_server/src/analysis/tokens.rs b/crates/language_server/src/analysis/tokens.rs index 53f5fbee307..b4ad0b00a3a 100644 --- a/crates/language_server/src/analysis/tokens.rs +++ b/crates/language_server/src/analysis/tokens.rs @@ -5,9 +5,10 @@ use bumpalo::{ use roc_module::called_via::{BinOp, UnaryOp}; use roc_parse::{ ast::{ - AbilityImpls, AbilityMember, AssignedField, Collection, Defs, Expr, Header, Implements, - ImplementsAbilities, ImplementsAbility, ImplementsClause, Pattern, PatternAs, Spaced, - StrLiteral, Tag, TypeAnnotation, TypeDef, TypeHeader, ValueDef, WhenBranch, + AbilityImpls, AbilityMember, AssignedField, Collection, Defs, DesugarProblem, Expr, Header, + Implements, ImplementsAbilities, ImplementsAbility, ImplementsClause, Pattern, PatternAs, + PrecedenceConflict, RecordFieldPattern, Spaced, StrLiteral, Tag, TypeAnnotation, TypeDef, + TypeHeader, ValueDef, WhenBranch, }, header::{ AppHeader, ExposedName, HostedHeader, ImportsEntry, ModuleHeader, ModuleName, ModuleParams, @@ -432,13 +433,23 @@ where { fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { match self { - AssignedField::RequiredValue(field, _, ty) - | AssignedField::OptionalValue(field, _, ty) - | AssignedField::IgnoredValue(field, _, ty) => (field_token(field.region, arena) - .into_iter()) - .chain(ty.iter_tokens(arena)) - .collect_in(arena), - AssignedField::LabelOnly(s) => s.iter_tokens(arena), + AssignedField::WithValue { + ignored: _, + loc_label, + before_prefix: _, + prefix: _, + loc_val, + } => (field_token(loc_label.region, arena).into_iter()) + .chain(loc_val.iter_tokens(arena)) + .collect_in(arena), + AssignedField::WithoutValue { + ignored: _, + loc_label, + } => loc_label.iter_tokens(arena), + AssignedField::SpreadValue(opt_spread) => opt_spread + .iter() + .flat_map(|spread| spread.iter_tokens(arena)) + .collect_in(arena), AssignedField::SpaceBefore(af, _) | AssignedField::SpaceAfter(af, _) => { af.iter_tokens(arena) } @@ -446,6 +457,35 @@ where } } +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self.value { + RecordFieldPattern::RequiredField { label: _, inner } => { + field_token(self.region, arena) + .into_iter() + .chain(inner.iter_tokens(arena)) + .collect_in(arena) + } + RecordFieldPattern::OptionalField { + label: _, + default_value, + } => field_token(self.region, arena) + .into_iter() + .chain(default_value.iter_tokens(arena)) + .collect_in(arena), + RecordFieldPattern::Identifier { label: _ } => field_token(self.region, arena), + RecordFieldPattern::Spread { opt_pattern } => opt_pattern + .iter() + .flat_map(|pat| pat.iter_tokens(arena)) + .collect_in(arena), + RecordFieldPattern::SpaceBefore(inner, _) + | RecordFieldPattern::SpaceAfter(inner, _) => { + Loc::at(self.region, *inner).iter_tokens(arena) + } + } + } +} + impl IterTokens for Loc> { fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { self.value.iter_tokens(arena) @@ -722,12 +762,18 @@ impl IterTokens for Loc> { Loc::at(region, *e).iter_tokens(arena) } Expr::ParensAround(e) => Loc::at(region, *e).iter_tokens(arena), - Expr::EmptyRecordBuilder(e) => e.iter_tokens(arena), - Expr::SingleFieldRecordBuilder(e) => e.iter_tokens(arena), - Expr::OptionalFieldInRecordBuilder(_name, e) => e.iter_tokens(arena), - Expr::MalformedIdent(_, _) | Expr::PrecedenceConflict(_) => { - bumpvec![in arena;] - } + Expr::MalformedIdent(_, _) => bumpvec![in arena;], + Expr::DesugarProblem(desugar_problem) => match desugar_problem { + DesugarProblem::PrecedenceConflict(PrecedenceConflict { expr, .. }) => { + expr.iter_tokens(arena) + } + DesugarProblem::EmptyRecordBuilder(e) => e.iter_tokens(arena), + DesugarProblem::SingleFieldRecordBuilder(e) => e.iter_tokens(arena), + DesugarProblem::OptionalFieldInRecordBuilder(_name, e) => e.iter_tokens(arena), + DesugarProblem::SpreadInRecordBuilder { + opt_spread_expr, .. + } => opt_spread_expr.iter_tokens(arena), + }, } } } @@ -770,8 +816,6 @@ impl IterTokens for Loc> { .chain(p2.iter_tokens(arena)) .collect_in(arena), Pattern::RecordDestructure(ps) => ps.iter_tokens(arena), - Pattern::RequiredField(_field, p) => p.iter_tokens(arena), - Pattern::OptionalField(_field, p) => p.iter_tokens(arena), Pattern::NumLiteral(_) => onetoken(Token::Number, region, arena), Pattern::NonBase10Literal { .. } => onetoken(Token::Number, region, arena), Pattern::FloatLiteral(_) => onetoken(Token::Number, region, arena), diff --git a/crates/repl_eval/src/eval.rs b/crates/repl_eval/src/eval.rs index cc853c7f527..80c41433213 100644 --- a/crates/repl_eval/src/eval.rs +++ b/crates/repl_eval/src/eval.rs @@ -1,5 +1,6 @@ use bumpalo::collections::{CollectIn, Vec}; use bumpalo::Bump; +use roc_parse::expr::RecordValuePrefix; use roc_types::types::AliasKind; use std::cmp::{max_by_key, min_by_key}; @@ -214,7 +215,13 @@ fn apply_newtypes<'a>( NewtypeKind::RecordField(field_name) => { let label = Loc::at_zero(field_name.as_str()); let field_val = arena.alloc(Loc::at_zero(expr)); - let field = Loc::at_zero(AssignedField::RequiredValue(label, &[], field_val)); + let field = Loc::at_zero(AssignedField::WithValue { + ignored: false, + loc_label: label, + before_prefix: &[], + prefix: RecordValuePrefix::Colon, + loc_val: field_val, + }); expr = Expr::Record(Collection::with_items(&*arena.alloc([field]))) } NewtypeKind::Opaque(name) => { @@ -1119,7 +1126,13 @@ fn struct_to_ast<'a, M: ReplAppMemory>( region: Region::zero(), }; let loc_field = Loc { - value: AssignedField::RequiredValue(field_name, &[], loc_expr), + value: AssignedField::WithValue { + ignored: false, + loc_label: field_name, + before_prefix: &[], + prefix: RecordValuePrefix::Colon, + loc_val: loc_expr, + }, region: Region::zero(), }; @@ -1175,7 +1188,13 @@ fn struct_to_ast<'a, M: ReplAppMemory>( region: Region::zero(), }; let loc_field = Loc { - value: AssignedField::RequiredValue(field_name, &[], loc_expr), + value: AssignedField::WithValue { + ignored: false, + loc_label: field_name, + before_prefix: &[], + prefix: RecordValuePrefix::Colon, + loc_val: loc_expr, + }, region: Region::zero(), }; @@ -1188,7 +1207,10 @@ fn struct_to_ast<'a, M: ReplAppMemory>( // to the user we want to present the fields in alphabetical order again, so re-sort fn sort_key<'a, T>(loc_field: &'a Loc>) -> &'a str { match &loc_field.value { - AssignedField::RequiredValue(field_name, _, _) => field_name.value, + AssignedField::WithValue { + loc_label: field_name, + .. + } => field_name.value, _ => unreachable!("was not added to output"), } } diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index 9e38647bf02..9579d78c8cc 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -2212,7 +2212,11 @@ fn pretty_runtime_error<'b>( title = INGESTED_FILE_ERROR; } - RuntimeError::InvalidPrecedence(BothNonAssociative(region, left_bin_op, right_bin_op), outer_region) => { + RuntimeError::InvalidPrecedence(BothNonAssociative( + whole_region, + left_bin_op, + right_bin_op, + )) => { doc = alloc.stack([ if left_bin_op.value == right_bin_op.value { alloc.concat([ @@ -2235,7 +2239,7 @@ fn pretty_runtime_error<'b>( )), ]) }, - alloc.region(lines.convert_region(region), severity), + alloc.region(lines.convert_region(whole_region), severity), ]); title = "INVALID PRECEDENCE"; diff --git a/crates/reporting/src/error/parse.rs b/crates/reporting/src/error/parse.rs index bea391a060d..034c0653951 100644 --- a/crates/reporting/src/error/parse.rs +++ b/crates/reporting/src/error/parse.rs @@ -2394,16 +2394,16 @@ fn to_precord_report<'a>( } _ => { let doc = alloc.stack([ - alloc.reflow("I am partway through parsing a record pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region, severity), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing curly brace before this, so try adding a ", - ), - alloc.parser_suggestion("}"), - alloc.reflow(" and see if that helps?"), - ]), - ]); + alloc.reflow("I am partway through parsing a record pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region, severity), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing curly brace before this, so try adding a ", + ), + alloc.parser_suggestion("}"), + alloc.reflow(" and see if that helps?"), + ]), + ]); Report { filename, @@ -2471,6 +2471,10 @@ fn to_precord_report<'a>( unreachable!("because `foo` is a valid field; the question mark is not required") } + PRecord::Spread(_) => { + unreachable!("if the spread fails to parse, we try to parse something else, so we can't fail here") + } + PRecord::Pattern(pattern, pos) => to_pattern_report(alloc, lines, filename, pattern, pos), PRecord::Expr(expr, pos) => to_expr_report( diff --git a/crates/reporting/src/error/type.rs b/crates/reporting/src/error/type.rs index 098ef9c8462..e52ba6a0cf8 100644 --- a/crates/reporting/src/error/type.rs +++ b/crates/reporting/src/error/type.rs @@ -2547,6 +2547,32 @@ fn to_pattern_report<'b>( severity, } } + // TODO: remove if we can't actually generate this! + PReason::SpreadGuard => { + let doc = alloc.stack([ + alloc.concat([alloc.reflow( + "This spread pattern doesn't align with the remaining fields in the :", + )]), + alloc.region(lines.convert_region(region), severity), + pattern_type_comparison( + alloc, + found, + expected_type, + add_pattern_category(alloc, alloc.text("It matches"), &category), + alloc.concat([ + alloc.text("But the other elements in this list pattern match") + ]), + vec![], + ), + ]); + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc, + severity, + } + } PReason::TagArg { .. } | PReason::PatternGuard => { internal_error!("We didn't think this could trigger. Please tell us about it on Zulip if it does!") } @@ -2594,6 +2620,8 @@ fn add_pattern_category<'b>( EmptyRecord => alloc.reflow(" an empty record:"), PatternGuard => alloc.reflow(" a pattern guard of type:"), PatternDefault => alloc.reflow(" an optional field of type:"), + NonCapturingSpread => alloc.reflow(" a non-capturing spread:"), + CapturingSpread => alloc.reflow(" a capturing spread of type:"), Set => alloc.reflow(" sets of type:"), Map => alloc.reflow(" maps of type:"), List => alloc.reflow(" lists of type:"), @@ -2603,6 +2631,7 @@ fn add_pattern_category<'b>( alloc.reflow(" tag of type:"), ]), Opaque(opaque) => alloc.concat([ + alloc.reflow(" "), alloc.opaque_name(*opaque), alloc.reflow(" unwrappings of type:"), ]), @@ -5578,7 +5607,10 @@ fn pattern_to_doc_help<'b>( alloc.text(" clause)"), ]) } - RenderAs::Record(field_names) => { + RenderAs::Record { + fields: field_names, + opt_spread, + } => { let mut arg_docs = Vec::with_capacity(args.len()); for (label, v) in field_names.into_iter().zip(args.into_iter()) { @@ -5600,6 +5632,15 @@ fn pattern_to_doc_help<'b>( alloc .text("{ ") .append(alloc.intersperse(arg_docs, alloc.reflow(", "))) + .append(match opt_spread { + None => alloc.text(""), + Some(spread) => match &*spread { + None => alloc.text(".."), + Some(spread_pat) => alloc + .text("..") + .append(pattern_to_doc_help(alloc, spread_pat, false)), + }, + }) .append(" }") } RenderAs::Tuple => {