diff --git a/core/src/eval/mod.rs b/core/src/eval/mod.rs index bc6ec2ae31..b8f7bd6ba5 100644 --- a/core/src/eval/mod.rs +++ b/core/src/eval/mod.rs @@ -1189,9 +1189,6 @@ pub fn subst( RichTerm::new(Term::App(t1, t2), pos) } Term::Match(data) => { - let default = - data.default.map(|d| subst(cache, d, initial_env, env)); - let branches = data.branches .into_iter() .map(|(pat, branch)| { @@ -1202,7 +1199,7 @@ pub fn subst( }) .collect(); - RichTerm::new(Term::Match(MatchData { branches, default}), pos) + RichTerm::new(Term::Match(MatchData { branches }), pos) } Term::Op1(op, t) => { let t = subst(cache, t, initial_env, env); diff --git a/core/src/parser/grammar.lalrpop b/core/src/parser/grammar.lalrpop index d38cc4bf91..5155a1b409 100644 --- a/core/src/parser/grammar.lalrpop +++ b/core/src/parser/grammar.lalrpop @@ -308,27 +308,13 @@ Applicative: UniTerm = { => UniTerm::from(mk_term::op2(op, t1, t2)), NOpPre>, "match" "{" "}" => { - let mut default = None; - let branches = branches .into_iter() .map(|(case, _comma)| case) .chain(last) - .filter_map(|case| match case { - MatchCase::Normal(pat, branch) => Some((pat, branch)), - MatchCase::Default(default_branch) => { - default = Some(default_branch); - None - } - }) .collect(); - UniTerm::from( - Term::Match(MatchData { - branches, - default, - }) - ) + UniTerm::from(Term::Match(MatchData { branches })) } }; @@ -578,6 +564,7 @@ PatternDataF: PatternData = { ConstantPattern => PatternData::Constant(<>), EnumPatternF => PatternData::Enum(<>), Ident => PatternData::Any(<>), + "_" => PatternData::Wildcard, }; // A general pattern. @@ -888,17 +875,8 @@ UOp: UnaryOp = { "enum_get_tag" => UnaryOp::EnumGetTag(), } -// It might seem silly that a match case can always be the catch-all case -// `_ => `. It would be better to separate between a normal match case and -// a rule for the catch-call. However, it's then surprisingly annoying to -// express the rule for "match" such that it's both non-ambiguous and allow an -// optional trailing comma ",". -// -// In the end, it was simpler to just allow the catch-all case to appear -// anywhere, and then to raise an error in the action code of the "match" rule. -MatchCase: MatchCase = { - "=>" => MatchCase::Normal(pat, t), - "_" "=>" => MatchCase::Default(<>), +MatchCase: (Pattern, RichTerm) = { + "=>" => (pat, t), }; // Infix operators by precedence levels. Lowest levels take precedence over diff --git a/core/src/parser/utils.rs b/core/src/parser/utils.rs index 5759b7acae..e5152f6a7b 100644 --- a/core/src/parser/utils.rs +++ b/core/src/parser/utils.rs @@ -79,13 +79,6 @@ pub enum StringEndDelimiter { Special, } -/// Distinguish between a normal case `id => exp` and a default case `_ => exp`. -#[derive(Clone, Debug)] -pub enum MatchCase { - Normal(Pattern, RichTerm), - Default(RichTerm), -} - /// Left hand side of a record field declaration. #[derive(Clone, Debug)] pub enum FieldPathElem { diff --git a/core/src/pretty.rs b/core/src/pretty.rs index 929e29e8ae..3bd179007e 100644 --- a/core/src/pretty.rs +++ b/core/src/pretty.rs @@ -585,6 +585,7 @@ where { fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> { match self { + PatternData::Wildcard => allocator.text("_"), PatternData::Any(id) => allocator.as_string(id), PatternData::Record(rp) => rp.pretty(allocator), PatternData::Enum(evp) => evp.pretty(allocator), @@ -886,7 +887,6 @@ where data.branches .iter() .map(|(pat, t)| (pat.pretty(allocator), t)) - .chain(data.default.iter().map(|d| (allocator.text("_"), d))) .map(|(lhs, t)| docs![ allocator, lhs, diff --git a/core/src/term/mod.rs b/core/src/term/mod.rs index 44c00ad2f0..d63496d4d1 100644 --- a/core/src/term/mod.rs +++ b/core/src/term/mod.rs @@ -610,7 +610,6 @@ pub struct MatchData { /// Branches of the match expression, where the first component is the pattern on the left hand /// side of `=>` and the second component is the body of the branch. pub branches: Vec<(Pattern, RichTerm)>, - pub default: Option, } /// A type or a contract together with its corresponding label. @@ -2032,12 +2031,9 @@ impl Traverse for RichTerm { .map(|(pat, t)| t.traverse(f, order).map(|t_ok| (pat, t_ok))) .collect(); - let default = data.default.map(|t| t.traverse(f, order)).transpose()?; - RichTerm::new( Term::Match(MatchData { branches: branches?, - default, }), pos, ) @@ -2210,8 +2206,7 @@ impl Traverse for RichTerm { Term::Match(data) => data .branches .iter() - .find_map(|(_pat, t)| t.traverse_ref(f, state)) - .or_else(|| data.default.as_ref().and_then(|t| t.traverse_ref(f, state))), + .find_map(|(_pat, t)| t.traverse_ref(f, state)), Term::Array(ts, _) => ts.iter().find_map(|t| t.traverse_ref(f, state)), Term::OpN(_, ts) => ts.iter().find_map(|t| t.traverse_ref(f, state)), Term::Annotated(annot, t) => t diff --git a/core/src/term/pattern/compile.rs b/core/src/term/pattern/compile.rs index f84cde2d65..10ee567821 100644 --- a/core/src/term/pattern/compile.rs +++ b/core/src/term/pattern/compile.rs @@ -252,6 +252,7 @@ impl CompilePart for Pattern { impl CompilePart for PatternData { fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm { match self { + PatternData::Wildcard => Term::Var(bindings_id).into(), PatternData::Any(id) => { // %record_insert% "" value_id bindings_id insert_binding(*id, value_id, bindings_id) @@ -666,35 +667,61 @@ impl Compile for MatchData { // else // # this primop evaluates body with an environment extended with bindings_id // %pattern_branch% body bindings_id - fn compile(self, value: RichTerm, pos: TermPos) -> RichTerm { + fn compile(mut self, value: RichTerm, pos: TermPos) -> RichTerm { if self.branches.iter().all(|(pat, _)| { matches!( pat.data, - PatternData::Enum(EnumPattern { pattern: None, .. }) + PatternData::Enum(EnumPattern { pattern: None, .. }) | PatternData::Wildcard ) }) { - let tags_only = self.branches.into_iter().map(|(pat, body)| { - let PatternData::Enum(EnumPattern {tag, ..}) = pat.data else { - panic!("match compilation: just tested that all cases are enum tags, but found a non enum tag pattern"); - }; + let wildcard_pat = self + .branches + .iter() + .enumerate() + .find_map(|(idx, (pat, body))| { + if let PatternData::Wildcard = pat.data { + Some((idx, body.clone())) + } else { + None + } + }); + + // If we find a wildcard pattern, we record its index in order to discard all the + // patterns coming after the wildcard, because they are unreachable. + let default = if let Some((idx, body)) = wildcard_pat { + self.branches.truncate(idx + 1); + Some(body) + } else { + None + }; - (tag, body) - }).collect(); + let tags_only = self + .branches + .into_iter() + .filter_map(|(pat, body)| { + if let PatternData::Enum(EnumPattern { tag, .. }) = pat.data { + Some((tag, body)) + } else { + None + } + }) + .collect(); return TagsOnlyMatch { branches: tags_only, - default: self.default, + default, } .compile(value, pos); } - let default_branch = self.default.unwrap_or_else(|| { + let error_case = RichTerm::new( Term::RuntimeError(EvalError::NonExhaustiveMatch { value: value.clone(), pos, - }) - .into() - }); + }), + pos, + ); + let value_id = LocIdent::fresh(); // The fold block: @@ -711,45 +738,45 @@ impl Compile for MatchData { // else // # this primop evaluates body with an environment extended with bindings_id // %pattern_branch% body bindings_id - let fold_block = - self.branches - .into_iter() - .rev() - .fold(default_branch, |cont, (pat, body)| { - let init_bindings_id = LocIdent::fresh(); - let bindings_id = LocIdent::fresh(); - - // inner if block: - // - // if bindings_id == null then - // cont - // else - // # this primop evaluates body with an environment extended with bindings_id - // %pattern_branch% bindings_id body - let inner = make::if_then_else( - make::op2(BinaryOp::Eq(), Term::Var(bindings_id), Term::Null), - cont, - mk_app!( - make::op1(UnaryOp::PatternBranch(), Term::Var(bindings_id),), - body - ), - ); + let fold_block = self + .branches + .into_iter() + .rev() + .fold(error_case, |cont, (pat, body)| { + let init_bindings_id = LocIdent::fresh(); + let bindings_id = LocIdent::fresh(); + + // inner if block: + // + // if bindings_id == null then + // cont + // else + // # this primop evaluates body with an environment extended with bindings_id + // %pattern_branch% bindings_id body + let inner = make::if_then_else( + make::op2(BinaryOp::Eq(), Term::Var(bindings_id), Term::Null), + cont, + mk_app!( + make::op1(UnaryOp::PatternBranch(), Term::Var(bindings_id),), + body + ), + ); - // The two initial chained let-bindings: - // - // let init_bindings_id = {} in - // let bindings_id = in - // + // The two initial chained let-bindings: + // + // let init_bindings_id = {} in + // let bindings_id = in + // + make::let_in( + init_bindings_id, + Term::Record(RecordData::empty()), make::let_in( - init_bindings_id, - Term::Record(RecordData::empty()), - make::let_in( - bindings_id, - pat.compile_part(value_id, init_bindings_id), - inner, - ), - ) - }); + bindings_id, + pat.compile_part(value_id, init_bindings_id), + inner, + ), + ) + }); // let value_id = value in make::let_in(value_id, value, fold_block) diff --git a/core/src/term/pattern/mod.rs b/core/src/term/pattern/mod.rs index 275d887ee5..fa3854599c 100644 --- a/core/src/term/pattern/mod.rs +++ b/core/src/term/pattern/mod.rs @@ -22,6 +22,9 @@ pub mod compile; #[derive(Debug, PartialEq, Clone)] pub enum PatternData { + /// A wildcard pattern, matching any value. As opposed to any, this pattern doesn't bind any + /// variable. + Wildcard, /// A simple pattern consisting of an identifier. Match anything and bind the result to the /// corresponding identfier. Any(LocIdent), @@ -215,7 +218,7 @@ pub trait ElaborateContract { impl ElaborateContract for PatternData { fn elaborate_contract(&self) -> Option { match self { - PatternData::Any(_) => None, + PatternData::Wildcard | PatternData::Any(_) => None, PatternData::Record(pat) => pat.elaborate_contract(), PatternData::Enum(pat) => pat.elaborate_contract(), PatternData::Constant(pat) => pat.elaborate_contract(), diff --git a/core/src/transform/desugar_destructuring.rs b/core/src/transform/desugar_destructuring.rs index e6c3264f8e..d571142374 100644 --- a/core/src/transform/desugar_destructuring.rs +++ b/core/src/transform/desugar_destructuring.rs @@ -136,6 +136,7 @@ impl Desugar for Pattern { impl Desugar for PatternData { fn desugar(self, destr: RichTerm, body: RichTerm) -> Term { match self { + PatternData::Wildcard => body.into(), // If the pattern is an unconstrained identifier, we just bind it to the value. PatternData::Any(id) => Term::Let(id, destr, body, LetAttrs::default()), PatternData::Record(pat) => pat.desugar(destr, body), diff --git a/core/src/transform/free_vars.rs b/core/src/transform/free_vars.rs index 8cc58620bb..e8f86c7d92 100644 --- a/core/src/transform/free_vars.rs +++ b/core/src/transform/free_vars.rs @@ -95,10 +95,6 @@ impl CollectFreeVars for RichTerm { free_vars.extend(fresh); } - - if let Some(default) = &mut data.default { - default.collect_free_vars(free_vars); - } } Term::Op1(_, t) | Term::Sealed(_, t, _) | Term::EnumVariant { arg: t, .. } => { t.collect_free_vars(free_vars) @@ -255,8 +251,8 @@ impl RemoveBindings for PatternData { PatternData::Enum(enum_variant_pat) => { enum_variant_pat.remove_bindings(working_set); } - // A constant pattern doesn't bind any variable. - PatternData::Constant(_) => (), + // A wildcard pattern or a constant pattern doesn't bind any variable. + PatternData::Wildcard | PatternData::Constant(_) => (), } } } diff --git a/core/src/typ.rs b/core/src/typ.rs index 52fb1eedf0..10f9caade6 100644 --- a/core/src/typ.rs +++ b/core/src/typ.rs @@ -1030,26 +1030,35 @@ impl Subcontract for EnumRows { } } - let default = if let Some(var) = tail_var { - mk_app!( - mk_term::op2( - BinaryOp::ApplyContract(), - get_var_contract(&vars, var.ident(), var.pos)?, - mk_term::var(label_arg) + let (default, default_pos) = if let Some(var) = tail_var { + ( + mk_app!( + mk_term::op2( + BinaryOp::ApplyContract(), + get_var_contract(&vars, var.ident(), var.pos)?, + mk_term::var(label_arg) + ), + mk_term::var(value_arg) ), - mk_term::var(value_arg) + var.pos, ) } else { - mk_app!(internals::enum_fail(), mk_term::var(label_arg)) + ( + mk_app!(internals::enum_fail(), mk_term::var(label_arg)), + TermPos::None, + ) }; - let match_expr = mk_app!( - Term::Match(MatchData { - branches, - default: Some(default) - }), - mk_term::var(value_arg) - ); + branches.push(( + Pattern { + data: PatternData::Wildcard, + alias: None, + pos: default_pos, + }, + default, + )); + + let match_expr = mk_app!(Term::Match(MatchData { branches }), mk_term::var(value_arg)); let case = mk_fun!(label_arg, value_arg, match_expr); Ok(mk_app!(internals::enumeration(), case)) diff --git a/core/src/typecheck/mod.rs b/core/src/typecheck/mod.rs index 8f8714ba8a..3fa71b905c 100644 --- a/core/src/typecheck/mod.rs +++ b/core/src/typecheck/mod.rs @@ -1551,10 +1551,6 @@ fn walk( walk(state, local_ctxt, visitor, branch) })?; - if let Some(default) = &data.default { - walk(state, ctxt, visitor, default)?; - } - Ok(()) } Term::RecRecord(record, dynamic, ..) => { @@ -1878,7 +1874,7 @@ fn check( pat.data .pattern_types(state, &ctxt, pattern::TypecheckMode::Enforce)?; // In the destructuring case, there's no alternative pattern, and we must thus - // immediatly close all the row types. + // immediately close all the row types. pattern::close_all_enums(pat_types.enum_open_tails, state); let src = pat_types.typ; @@ -2033,10 +2029,6 @@ fn check( check(state, ctxt.clone(), visitor, arm, return_type.clone())?; } - if let Some(default) = &data.default { - check(state, ctxt.clone(), visitor, default, return_type.clone())?; - } - let pat_types = with_pat_types .into_iter() .map(|(_, pat_types, _)| pat_types); @@ -2050,6 +2042,14 @@ fn check( .sum(), ); + // Build the list of all wildcard pattern occurrences + let mut wildcard_occurrences = HashSet::with_capacity( + pat_types + .clone() + .map(|pat_type| pat_type.wildcard_occurrences.len()) + .sum(), + ); + // We don't immediately return if an error occurs while unifying the patterns together. // For error reporting purposes, it's best to first close the tail variables (if // needed), to avoid cluttering the reported types with free unification variables @@ -2064,16 +2064,14 @@ fn check( } enum_open_tails.extend(pat_type.enum_open_tails); + wildcard_occurrences.extend(pat_type.wildcard_occurrences); Ok(()) }); - if data.default.is_some() { - // If there is a default value, we don't close the potential top-level enum type - pattern::close_enums(enum_open_tails, |path| !path.is_empty(), state); - } else { - pattern::close_all_enums(enum_open_tails, state); - } + // Once we have accumulated all the information about enum rows and wildcard + // occurrences, we can finally close the tails that need to be. + pattern::close_enums(enum_open_tails, &wildcard_occurrences, state); pat_unif_result.map_err(|err| err.into_typecheck_err(state, rt.pos))?; diff --git a/core/src/typecheck/pattern.rs b/core/src/typecheck/pattern.rs index 3299365589..d3d8075dec 100644 --- a/core/src/typecheck/pattern.rs +++ b/core/src/typecheck/pattern.rs @@ -14,6 +14,7 @@ pub(super) enum TypecheckMode { Enforce, } +/// A list of pattern variables and their associated type. pub type TypeBindings = Vec<(LocIdent, UnifType)>; /// An element of a pattern path. A pattern path is a sequence of steps that can be used to @@ -23,7 +24,7 @@ pub type TypeBindings = Vec<(LocIdent, UnifType)>; /// /// - The path of the full pattern within itself is the empty path. /// - The path of the `arg` pattern is `[Field("foo"), Field("bar"), Variant]`. -#[derive(Debug, Clone, PartialEq, Eq, Copy)] +#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)] pub enum PatternPathElem { Field(Ident), Variant, @@ -33,8 +34,14 @@ pub type PatternPath = Vec; /// The working state of [PatternType::pattern_types_inj]. pub(super) struct PatTypeState<'a> { + /// The list of pattern variables introduced so far and their inferred type. bindings: &'a mut TypeBindings, + /// The list of enum row tail variables that are left open when typechecking a match expression. enum_open_tails: &'a mut Vec<(PatternPath, UnifEnumRows)>, + /// Record, as a field path, the position of wildcard pattern encountered in a record. This + /// impact the final type of the pattern, as a wildcard pattern makes the corresponding row + /// open. + wildcard_pat_paths: &'a mut HashSet, } /// Return value of [PatternTypes::pattern_types], which stores the overall type of a pattern, @@ -50,7 +57,7 @@ pub struct PatternTypeData { /// /// Those variables (or their descendent in a row type) might need to be closed after the type /// of all the patterns of a match expression have been unified, depending on the presence of a - /// default case. The path of the corresponding sub-pattern is stored as well, since enum + /// wildcard pattern. The path of the corresponding sub-pattern is stored as well, since enum /// patterns in different positions might need different treatment. For example: /// /// ```nickel @@ -64,48 +71,58 @@ pub struct PatternTypeData { /// The presence of a default case means that the row variables of top-level enum patterns /// might stay open. However, the type corresponding to the sub-patterns `'Bar x` and `'Qux x` /// must be closed, because this match expression can't handle `'Foo ('Other 0)`. The type of - /// the expression is thus `[| 'Foo [| 'Bar: a, 'Qux: b |]; c|] -> d`. + /// the match expression is thus `[| 'Foo [| 'Bar: a, 'Qux: b |]; c|] -> d`. /// - /// Currently, only the top-level enum patterns can have a default case, hence only the - /// top-leve enum patterns might stay open. However, this might change in the future, as a - /// wildcard pattern `_` is common and could then appear at any level, making the potential - /// other enum located at the same path to stay open as well. + /// Wildcard can occur anywhere, so the previous case can also happen within a record pattern: + /// + /// ```nickel + /// match { + /// {foo = 'Bar x} => , + /// {foo = 'Qux x} => , + /// {foo = _} => , + /// } + /// ``` + /// + /// Similarly, the type of the match expression is `{ foo: [| 'Bar: a, 'Qux: b; c |] } -> e`. /// /// See [^typechecking-match-expression] in [typecheck] for more details. pub enum_open_tails: Vec<(PatternPath, UnifEnumRows)>, + /// Paths of the occurrence of wildcard patterns encountered. This is used to determine which + /// tails in [Self::enum_open_tails] should be left open. + pub wildcard_occurrences: HashSet, } -/// Close all the enum row types left open when typechecking a match expression whose path matches -/// the given filter. +/// Close all the enum row types left open when typechecking a match expression. Special case of +/// `close_enums` for a single destructuring pattern (thus, where wildcard occurrences are not +/// relevant). +pub fn close_all_enums(enum_open_tails: Vec<(PatternPath, UnifEnumRows)>, state: &mut State) { + close_enums(enum_open_tails, &HashSet::new(), state); +} + +/// Close all the enum row types left open when typechecking a match expression, unless we recorded +/// a wildcard pattern somewhere in the same position. pub fn close_enums( enum_open_tails: Vec<(PatternPath, UnifEnumRows)>, - mut filter: impl FnMut(&PatternPath) -> bool, + wildcard_occurrences: &HashSet, state: &mut State, ) { - enum_open_tails - .into_iter() - .filter_map(|(path, tail)| filter(&path).then_some(tail)) - .for_each(|tail| { - close_enum(tail, state); - }) -} - -/// Close all the enum row types left open when typechecking a match expression. -pub fn close_all_enums(enum_open_tails: Vec<(PatternPath, UnifEnumRows)>, state: &mut State) { // Note: both for this function and for `close_enums`, for a given pattern path, all the tail // variables should ultimately be part of the same enum type, and we just need to close it // once. We might thus save a bit of work if we kept equivalence classes of tuples (path, tail) // (equality being given by the equality of paths). Closing one arbitrary member per class // should then be enough. It's not obvious that this would make any difference in practice, // though. - for (_path, tail) in enum_open_tails { + for tail in enum_open_tails + .into_iter() + .filter_map(|(path, tail)| (!wildcard_occurrences.contains(&path)).then_some(tail)) + { close_enum(tail, state); } } /// Take an enum row, find its final tail (in case of multiple indirection through unification /// variables) and close it if it's a free unification variable. -pub fn close_enum(tail: UnifEnumRows, state: &mut State) { +fn close_enum(tail: UnifEnumRows, state: &mut State) { let root = tail.into_root(state.table); if let UnifEnumRows::UnifVar { id, .. } = root { @@ -170,11 +187,13 @@ pub(super) trait PatternTypes { ) -> Result, TypecheckError> { let mut bindings = Vec::new(); let mut enum_open_tails = Vec::new(); + let mut wildcard_pat_paths = HashSet::new(); let typ = self.pattern_types_inj( &mut PatTypeState { bindings: &mut bindings, enum_open_tails: &mut enum_open_tails, + wildcard_pat_paths: &mut wildcard_pat_paths, }, Vec::new(), state, @@ -186,6 +205,7 @@ pub(super) trait PatternTypes { typ, bindings, enum_open_tails, + wildcard_occurrences: wildcard_pat_paths, }) } @@ -276,6 +296,15 @@ impl PatternTypes for Pattern { } } +// Depending on the mode, returns the type affected to patterns that match any value (`Any` and +// `Wildcard`): `Dyn` in walk mode, a fresh unification variable in enforce mode. +fn any_type(mode: TypecheckMode, state: &mut State, ctxt: &Context) -> UnifType { + match mode { + TypecheckMode::Walk => mk_uniftype::dynamic(), + TypecheckMode::Enforce => state.table.fresh_type_uvar(ctxt.var_level), + } +} + impl PatternTypes for PatternData { type PatType = UnifType; @@ -288,12 +317,12 @@ impl PatternTypes for PatternData { mode: TypecheckMode, ) -> Result { match self { + PatternData::Wildcard => { + pt_state.wildcard_pat_paths.insert(path); + Ok(any_type(mode, state, ctxt)) + } PatternData::Any(id) => { - let typ = match mode { - TypecheckMode::Walk => mk_uniftype::dynamic(), - TypecheckMode::Enforce => state.table.fresh_type_uvar(ctxt.var_level), - }; - + let typ = any_type(mode, state, ctxt); pt_state.bindings.push((*id, typ.clone())); Ok(typ) @@ -376,7 +405,7 @@ impl PatternTypes for FieldPattern { // and (2) that we assign the annotated types to the right unification variables. let ty_row = match (&self.annotation.typ, &self.pattern.data, mode) { // However, in walk mode, we only do that when the nested pattern isn't a leaf (i.e. - // `Any`) for backward-compatibility reasons. + // `Any` or `Wildcard`) for backward-compatibility reasons. // // Before this function was refactored, Nickel has been allowing things like `let {foo // : Number} = {foo = 1} in foo` in walk mode, which would fail to typecheck with the @@ -393,6 +422,9 @@ impl PatternTypes for FieldPattern { pt_state.bindings.push((*id, ty_row.clone())); ty_row } + (Some(annot_ty), PatternData::Wildcard, TypecheckMode::Walk) => { + UnifType::from_type(annot_ty.typ.clone(), &ctxt.term_env) + } (Some(annot_ty), _, _) => { let pos = annot_ty.typ.pos; let annot_uty = UnifType::from_type(annot_ty.typ.clone(), &ctxt.term_env); diff --git a/core/tests/integration/inputs/pattern-matching/enum_after_wildcard.ncl b/core/tests/integration/inputs/pattern-matching/enum_after_wildcard.ncl new file mode 100644 index 0000000000..c9efa32001 --- /dev/null +++ b/core/tests/integration/inputs/pattern-matching/enum_after_wildcard.ncl @@ -0,0 +1,31 @@ +# test.type = 'pass' + +# pattern matching on pure enum is special-cased for performance reasons, using +# a hashmap. The presence of a wildcard pattern has to be handled differently +# that for normal match expressions, which are just testing all patterns in +# order. +# +# This test ensure it correctly handle a wildcard being used before the end of +# the match expression, which was mishandled previously (branches after the +# wildcard would be selected if they matched). +let {check, ..} = import "../lib/assert.ncl" in + +[ + 'Foo |> match { + 'A => false, + 'B => false, + 'C => false, + _ => true, + 'Foo => false, + }, + # check for off-by-one error when truncating patterns coming after the + # wildcard + 'Foo |> match { + 'A => false, + 'B => false, + 'C => false, + 'Foo => true, + _ => false, + }, +] +|> check diff --git a/core/tests/integration/inputs/pattern-matching/wildcards.ncl b/core/tests/integration/inputs/pattern-matching/wildcards.ncl new file mode 100644 index 0000000000..7f849bbc77 --- /dev/null +++ b/core/tests/integration/inputs/pattern-matching/wildcards.ncl @@ -0,0 +1,24 @@ +# test.type = 'pass' +let {check, ..} = import "../lib/assert.ncl" in + +[ + {foo = 1, bar = 2} |> match { + {foo = _, baz = _} => false, + {foo = _, bar = _} => true, + }, + + 'Some ('Tag true) |> match { + 'None ('Tag x) => !x, + 'Some ('Other x) => !x, + 'Some ('Tag _) => true, + }, + + 'Point {x = 1, y = 2, z = 3} |> match { + 'Line _ => false, + 'Plane {u, v} => false, + 'Volume {u, v, w} => false, + 'Point {x} => false, + 'Point _ => true, + }, +] +|> check diff --git a/core/tests/integration/inputs/typecheck/pattern_matching.ncl b/core/tests/integration/inputs/typecheck/pattern_matching.ncl index 616f6eb209..730e8d883c 100644 --- a/core/tests/integration/inputs/typecheck/pattern_matching.ncl +++ b/core/tests/integration/inputs/typecheck/pattern_matching.ncl @@ -55,6 +55,20 @@ let typecheck = [ 'Real => "real", }) ) : _, + + # open enum rows when using wildcard + + match { + 'Some {foo = 'Bar 5, nested = 'One ('Two null)} => true, + 'Some {foo = 'Baz "str", nested = 'One ('Three null)} => true, + 'Some {foo = _, nested = 'One _} => false, + _ => false, + } : forall r1 r2 r3. + [| 'Some { + foo: [| 'Bar Number, 'Baz String; r1 |], + nested: [| 'One [| 'Two Dyn, 'Three Dyn; r2 |] |] }; + r3 + |] -> Bool, ] in true diff --git a/core/tests/integration/inputs/typecheck/pattern_variant_arg_mismatch_wildcard.ncl b/core/tests/integration/inputs/typecheck/pattern_variant_arg_mismatch_wildcard.ncl new file mode 100644 index 0000000000..74058004d9 --- /dev/null +++ b/core/tests/integration/inputs/typecheck/pattern_variant_arg_mismatch_wildcard.ncl @@ -0,0 +1,10 @@ +# test.type = 'error' +# eval = 'typecheck' +# +# [test.metadata] +# error = 'TypecheckError::EnumRowMismatch' +match { + 'Foo x => let _ = x + 1 in null, + 'Foo y => let _ = y ++ "a" in null, + 'Foo _ => null, + } : _ diff --git a/lsp/nls/src/pattern.rs b/lsp/nls/src/pattern.rs index 807f178400..18324926be 100644 --- a/lsp/nls/src/pattern.rs +++ b/lsp/nls/src/pattern.rs @@ -89,8 +89,8 @@ impl InjectBindings for PatternData { PatternData::Enum(evariant_pat) => { evariant_pat.inject_bindings(bindings, path, parent_deco) } - // Constant patterns don't bind any variable - PatternData::Constant(_) => (), + // Wildcard and constant patterns don't bind any variable + PatternData::Wildcard | PatternData::Constant(_) => (), } } }