Repository: gleam-lang/gleam (rust)

Issue:
Variant inference does not work with `let assert`
When using `let assert` the compiler will not use the information that a variable should be narrowed when performing record field access or update:

Give this type:

```gleam
type Example {
  Wibble
  Wobble(x: Int, y: Int)
}
```

The following two snippets fail to type check:

```gleam
fn access_reproduction(example: Example) {
  let assert Wobble(x:, ..) = example

  example.x
}
```

```gleam
fn update_reproduction(example: Example) {
  let assert Wobble(x:, ..) = example

  Wobble(..example, x: x + 1)
}
```

With these errors:

```
error: Unknown record field
    ┌─ /...
    │
xyz │   example.x
    │           ^ This field does not exist

The value being accessed has this type:

    Example

It does not have fields that are common across all variants.

Note: The field you are trying to access is not defined consistently across
all variants of this custom type. To fix this, ensure that all variants
include the field with the same name, position, and type.
```

```
error: Unsafe record update
    ┌─ /...
    │
xyz │   Wobble(..example, x: x + 1)
    │            ^^^^^^^ I'm not sure this is always a `Wobble`


This value cannot be used to build an updated `Wobble` as it could be some
other variant.

Consider pattern matching on it with a case expression and then
constructing a new record with its values.
```

With the variant inference feature I would expect both examples to work

Pull request:
Make sure a variant is inferred when doing `let assert`
This PR closes #4350 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1253fbdee..00e5ef273 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -219,6 +219,9 @@
   if used inside a record update.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- Fixed a bug where variant inference wouldn't work on `let assert` assignments.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ## v1.9.1 - 2025-03-10
 
 ### Formatter
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index a06739448..16e5d7916 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -1473,7 +1473,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         };
 
         let type_ = value.type_();
-
         let kind = self.infer_assignment_kind(kind.clone());
 
         // Ensure the pattern matches the type of the value
@@ -1485,7 +1484,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
             &self.hydrator,
             self.problems,
         );
-        let unify_result = pattern_typer.unify(pattern, type_.clone(), None);
+
+        let value_variable_name = match value {
+            TypedExpr::Var { ref name, .. } => Some(name.clone()),
+            _ => None,
+        };
+        let unify_result = pattern_typer.unify(pattern, type_.clone(), value_variable_name);
 
         let minimum_required_version = pattern_typer.minimum_required_version;
         if minimum_required_version > self.minimum_required_version {
@@ -1521,16 +1525,26 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         // Do not perform exhaustiveness checking if user explicitly used `let assert ... = ...`.
         let exhaustiveness_check = self.check_let_exhaustiveness(location, value.type_(), &pattern);
         match (&kind, exhaustiveness_check) {
+            // The pattern is exhaustive in a let assignment, there's no problem here.
             (AssignmentKind::Let | AssignmentKind::Generated, Ok(_)) => {}
+
+            // If the pattern is not exhaustive and we're not asserting we want to
+            // report the error!
             (AssignmentKind::Let | AssignmentKind::Generated, Err(e)) => {
                 self.problems.error(e);
             }
+
+            // If we're asserting but the pattern already covers all cases then the
+            // `assert` is redundant and can be safely removed.
             (AssignmentKind::Assert { location, .. }, Ok(_)) => {
                 self.problems.warning(Warning::RedundantAssertAssignment {
                     location: *location,
                 })
             }
-            (AssignmentKind::Assert { .. }, _) => {}
+
+            // Otherwise we just don't care, asserting will ignore the any missing
+            // pattern error.
+            (AssignmentKind::Assert { .. }, Err(_)) => {}
         }
 
         Assignment {
@@ -4001,6 +4015,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         })
     }
 
+    /// Returns `Ok(())` if the let is exhaustive, returns an `InexhaustiveLetAssignment`
+    /// error if the given pattern doesn't cover all possible cases.
+    ///
     fn check_let_exhaustiveness(
         &self,
         location: SrcSpan,
@@ -4013,13 +4030,13 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
 
         // Error for missing clauses that would cause a crash
         if output.diagnostics.missing {
-            return Err(Error::InexhaustiveLetAssignment {
+            Err(Error::InexhaustiveLetAssignment {
                 location,
                 missing: output.missing_patterns(self.environment),
-            });
+            })
+        } else {
+            Ok(())
         }
-
-        Ok(())
     }
 
     fn check_case_exhaustiveness(
diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs
index eab1a6ec5..f51377f7a 100644
--- a/compiler-core/src/type_/pattern.rs
+++ b/compiler-core/src/type_/pattern.rs
@@ -660,7 +660,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                 // Register the value as seen for detection of unused values
                 self.environment.increment_usage(&name);
 
-                let cons = self
+                let constructor = self
                     .environment
                     .get_value_constructor(module.as_ref().map(|(module, _)| module), &name)
                     .map_err(|e| {
@@ -671,7 +671,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                         )
                     })?;
 
-                match cons.field_map() {
+                match constructor.field_map() {
                     // The fun has a field map so labelled arguments may be present and need to be reordered.
                     Some(field_map) => {
                         if let Some(spread_location) = spread {
@@ -692,7 +692,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                             // Potential future optimisation.
                             let index_of_first_labelled_arg = pattern_args
                                 .iter()
-                                .position(|a| a.label.is_some())
+                                .position(|argument| argument.label.is_some())
                                 .unwrap_or(pattern_args.len());
 
                             // In Gleam we can pass in positional unlabelled args to a constructor
@@ -720,7 +720,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                             let supplied_unlabelled_args = index_of_first_labelled_arg;
                             let supplied_labelled_args = pattern_args
                                 .iter()
-                                .filter_map(|l| l.label.clone())
+                                .filter_map(|argument| argument.label.clone())
                                 .collect::<HashSet<_>>();
                             let constructor_unlabelled_args =
                                 field_map.arity - field_map.fields.len() as u32;
@@ -732,7 +732,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                                 .fields
                                 .iter()
                                 // We take the labels in order of definition in the constructor...
-                                .sorted_by_key(|(_, pos)| *pos)
+                                .sorted_by_key(|(_, position)| *position)
                                 .map(|(label, _)| label.clone())
                                 // ...and then remove the ones that were supplied as unlabelled
                                 // positional arguments...
@@ -765,7 +765,9 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                         assert_no_labelled_arguments(&pattern_args)?;
 
                         if let Some(spread_location) = spread {
-                            if let ValueConstructorVariant::Record { arity, .. } = &cons.variant {
+                            if let ValueConstructorVariant::Record { arity, .. } =
+                                &constructor.variant
+                            {
                                 while pattern_args.len() < usize::from(*arity) {
                                     pattern_args.push(CallArg {
                                         value: Pattern::Discard {
@@ -783,8 +785,9 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                     }
                 }
 
-                let constructor_type = cons.type_.clone();
-                let constructor = match &cons.variant {
+                let constructor_type = constructor.type_.clone();
+                let constructor_deprecation = constructor.deprecation.clone();
+                let pattern_constructor = match &constructor.variant {
                     ValueConstructorVariant::Record {
                         name,
                         documentation,
@@ -792,14 +795,26 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                         location,
                         variant_index: constructor_index,
                         ..
-                    } => PatternConstructor {
-                        documentation: documentation.clone(),
-                        name: name.clone(),
-                        field_map: cons.field_map().cloned(),
-                        module: module.clone(),
-                        location: *location,
-                        constructor_index: *constructor_index,
-                    },
+                    } => {
+                        let constructor_index = *constructor_index;
+                        let constructor = PatternConstructor {
+                            documentation: documentation.clone(),
+                            name: name.clone(),
+                            field_map: constructor.field_map().cloned(),
+                            module: module.clone(),
+                            location: *location,
+                            constructor_index,
+                        };
+
+                        if let Some(ref variable_name) = subject_variable {
+                            self.set_subject_variable_variant(
+                                variable_name.clone(),
+                                constructor_index,
+                            );
+                        }
+
+                        constructor
+                    }
                     ValueConstructorVariant::LocalVariable { .. }
                     | ValueConstructorVariant::LocalConstant { .. }
                     | ValueConstructorVariant::ModuleConstant { .. }
@@ -808,7 +823,6 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                     }
                 };
 
-                let constructor_deprecation = cons.deprecation.clone();
                 match constructor_deprecation {
                     Deprecation::NotDeprecated => {}
                     Deprecation::Deprecated { message } => {
@@ -821,8 +835,8 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                 }
 
                 self.environment.references.register_value_reference(
-                    constructor.module.clone(),
-                    constructor.name.clone(),
+                    pattern_constructor.module.clone(),
+                    pattern_constructor.name.clone(),
                     name_location,
                     if module.is_some() {
                         ReferenceKind::Qualified
@@ -881,7 +895,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                                 module,
                                 name,
                                 arguments: pattern_args,
-                                constructor: Inferred::Known(constructor),
+                                constructor: Inferred::Known(pattern_constructor),
                                 spread,
                                 type_: return_.clone(),
                             })
@@ -917,7 +931,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                                 module,
                                 name,
                                 arguments: vec![],
-                                constructor: Inferred::Known(constructor),
+                                constructor: Inferred::Known(pattern_constructor),
                                 spread,
                                 type_: instantiated_constructor_type,
                             })


Test patch:
diff --git a/compiler-core/src/type_/tests.rs b/compiler-core/src/type_/tests.rs
index caa720f2d..215691c5d 100644
--- a/compiler-core/src/type_/tests.rs
+++ b/compiler-core/src/type_/tests.rs
@@ -3069,6 +3069,51 @@ pub fn main() {
     );
 }
 
+#[test]
+fn variant_inference_with_let_assert() {
+    assert_module_infer!(
+        "
+type Wibble {
+  Wibble(n: Int, b: Bool)
+  Wobble
+}
+
+pub fn main() {
+  let wibble = wibble()
+  let assert Wibble(..) = wibble
+  wibble.n
+}
+
+fn wibble() {
+  Wibble(1, True)
+}
+",
+        vec![("main", "fn() -> Int")]
+    );
+}
+
+#[test]
+fn variant_inference_with_let_assert_and_alias() {
+    assert_module_infer!(
+        "
+type Wibble {
+  Wibble(n: Int, b: Bool)
+  Wobble
+}
+
+pub fn main() {
+  let assert Wibble(..) as wibble = wibble()
+  wibble.n
+}
+
+fn wibble() {
+  Wibble(1, True)
+}
+",
+        vec![("main", "fn() -> Int")]
+    );
+}
+
 #[test]
 fn private_types_not_available_in_other_modules() {
     assert_with_module_error!(