Repository: gleam-lang/gleam (rust)

Issue:
Type variables preserve record variant inference when inside tuples
(Gleam nightly)

```gleam
type Either(a, b) {
  Left(value: a)
  Right(value: b)
}

fn always_second(_first: value, second: value) -> value {
  second
}

pub fn main() {
  let #(right) = always_second(#(Left(5)), #(Right("hello")))
  Left(..right, inner: 10)
}
```

compiles, except for a "redundant record update" warning. It is important that the values here are wrapped in tuples - the size of the tuple doesn't matter, and a `Box` record works correctly.

There might be a second bug here about type unification not dropping the inferred variant, but it also works without having 2 different values for the compiler to infer:

```gleam
fn twice(state: state, fun: fn(state) -> state) -> state {
  fun(fun(state))
}

fn main() {
  twice(#(Left(5)), fn(value) {
    case value.0 {
      Left(_) -> #(Right("hello"))
      Right(_) -> value
    }
  })
}
``` 

produces

```txt
warning: Unreachable case clause
   ┌─ /home/arkan/Projects/oss/gleam/probe/src/probe.gleam:25:7
   │
25 │       Right(_) -> value
   │       ^^^^^^^^^^^^^^^^^

This case clause cannot be reached as it matches on a variant of a type
which is never present.
```

I'm sorry Gears ~ :purple_heart: 

Pull request:
Fix bug with variant inference in tuples
Fixes #3797

Patch:
diff --git a/.editorconfig b/.editorconfig
index 0c6dbd519..441c0da4e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -15,7 +15,7 @@ insert_final_newline = true
 indent_style = space
 indent_size = 2
 
-[*.{rs, gleam, erl, hrl, ex, mjs, js, ts, sh}]
+[*.{gleam, erl, hrl, ex, mjs, js, ts, sh}]
 max_line_length = 80
 
 [*.{rs, .erl, .hrl}]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46f59bad2..0792b024c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -378,6 +378,14 @@
   no arguments.
   ([Louis Pilfold](https://github.com/lpil))
 
+- Fixed a bug where the parser would incorrectly parse a generic type
+  constructor with no arguments.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
+- Fixed a bug where the parser would incorrectly parse a generic type definition
+  with no arguments.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 - Fixed a bug where the language server wouldn't show hints when hovering over
   the tail of a list.
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs
index 17cad973f..9b7f6b4a3 100644
--- a/compiler-core/src/parse.rs
+++ b/compiler-core/src/parse.rs
@@ -2215,10 +2215,16 @@ where
         &mut self,
     ) -> Result<(u32, EcoString, Vec<SpannedString>, u32, u32), ParseError> {
         let (start, upname, end) = self.expect_upname()?;
-        if self.maybe_one(&Token::LeftParen).is_some() {
+        if let Some((par_s, _)) = self.maybe_one(&Token::LeftParen) {
             let args =
                 Parser::series_of(self, &|p| Ok(Parser::maybe_name(p)), Some(&Token::Comma))?;
             let (_, par_e) = self.expect_one_following_series(&Token::RightParen, "a name")?;
+            if args.is_empty() {
+                return parse_error(
+                    ParseErrorType::TypeDefinitionNoArguments,
+                    SrcSpan::new(par_s, par_e),
+                );
+            }
             let args2 = args
                 .into_iter()
                 .map(|(start, name, end)| (SrcSpan { start, end }, name))
@@ -2408,9 +2414,15 @@ where
         name: EcoString,
         end: u32,
     ) -> Result<Option<TypeAst>, ParseError> {
-        if self.maybe_one(&Token::LeftParen).is_some() {
+        if let Some((par_s, _)) = self.maybe_one(&Token::LeftParen) {
             let args = self.parse_types()?;
             let (_, par_e) = self.expect_one(&Token::RightParen)?;
+            if args.is_empty() {
+                return parse_error(
+                    ParseErrorType::TypeConstructorNoArguments,
+                    SrcSpan::new(par_s, par_e),
+                );
+            }
             Ok(Some(TypeAst::Constructor(TypeAstConstructor {
                 location: SrcSpan { start, end: par_e },
                 module,
diff --git a/compiler-core/src/parse/error.rs b/compiler-core/src/parse/error.rs
index c973a8e71..e99e49eeb 100644
--- a/compiler-core/src/parse/error.rs
+++ b/compiler-core/src/parse/error.rs
@@ -316,6 +316,17 @@ utf16_codepoint, utf32_codepoint, signed, unsigned, big, little, native, size, u
                 "I was expecting arguments here",
                 vec!["A record must be passed arguments when constructed.".into()],
             ),
+            ParseErrorType::TypeConstructorNoArguments => (
+                "I was expecting arguments here",
+                vec!["A type constructor must be passed arguments.".into()],
+            ),
+            ParseErrorType::TypeDefinitionNoArguments => (
+                "I was expecting generic parameters here",
+                vec![
+                    "A generic type must have at least a generic parameter.".into(),
+                    "Hint: If a type is not generic you should omit the `()`.".into(),
+                ],
+            ),
         }
     }
 }
@@ -385,6 +396,8 @@ pub enum ParseErrorType {
     CallInClauseGuard, // case x { _ if f() -> 1 }
     IfExpression,
     ConstantRecordConstructorNoArguments, // const x = Record()
+    TypeConstructorNoArguments,           // let a : Int()
+    TypeDefinitionNoArguments,            // pub type Wibble() { ... }
 }
 
 impl LexicalError {
diff --git a/compiler-core/src/type_.rs b/compiler-core/src/type_.rs
index 6e2d0b229..32b694558 100644
--- a/compiler-core/src/type_.rs
+++ b/compiler-core/src/type_.rs
@@ -280,7 +280,17 @@ impl Type {
                 inferred_variant, ..
             } => *inferred_variant = None,
             Type::Var { type_ } => type_.borrow_mut().generalise_custom_type_variant(),
-            Type::Fn { .. } | Type::Tuple { .. } => {}
+            Type::Tuple { elems } => {
+                for element in elems {
+                    Arc::make_mut(element).generalise_custom_type_variant();
+                }
+            }
+            Type::Fn { args, retrn } => {
+                for argument in args {
+                    Arc::make_mut(argument).generalise_custom_type_variant();
+                }
+                Arc::make_mut(retrn).generalise_custom_type_variant();
+            }
         }
     }
 


Test patch:
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_annotation_1.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_annotation_1.snap
new file mode 100644
index 000000000..dde98fe3f
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_annotation_1.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: "pub fn main() -> Int() {}"
+---
+----- SOURCE CODE
+pub fn main() -> Int() {}
+
+----- ERROR
+error: Syntax error
+  ┌─ /src/parse/error.gleam:1:21
+  │
+1 │ pub fn main() -> Int() {}
+  │                     ^^ I was expecting arguments here
+
+A type constructor must be passed arguments.
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_annotation_2.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_annotation_2.snap
new file mode 100644
index 000000000..857fc1211
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_annotation_2.snap
@@ -0,0 +1,17 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: "pub fn main() {\n  let a: Int() = todo\n}"
+---
+----- SOURCE CODE
+pub fn main() {
+  let a: Int() = todo
+}
+
+----- ERROR
+error: Syntax error
+  ┌─ /src/parse/error.gleam:2:13
+  │
+2 │   let a: Int() = todo
+  │             ^^ I was expecting arguments here
+
+A type constructor must be passed arguments.
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_definition.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_definition.snap
new file mode 100644
index 000000000..1385e3466
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_type_constructor_arguments_in_type_definition.snap
@@ -0,0 +1,20 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: "\npub type A() {\n  A(Int)\n}\n"
+---
+----- SOURCE CODE
+
+pub type A() {
+  A(Int)
+}
+
+
+----- ERROR
+error: Syntax error
+  ┌─ /src/parse/error.gleam:2:11
+  │
+2 │ pub type A() {
+  │           ^^ I was expecting generic parameters here
+
+A generic type must have at least a generic parameter.
+Hint: If a type is not generic you should omit the `()`.
diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs
index ed674910a..c68c778ee 100644
--- a/compiler-core/src/parse/tests.rs
+++ b/compiler-core/src/parse/tests.rs
@@ -1548,3 +1548,29 @@ const a = A()
 "
     );
 }
+
+// https://github.com/gleam-lang/gleam/issues/3796
+#[test]
+fn missing_type_constructor_arguments_in_type_definition() {
+    assert_module_error!(
+        "
+pub type A() {
+  A(Int)
+}
+"
+    );
+}
+
+#[test]
+fn missing_type_constructor_arguments_in_type_annotation_1() {
+    assert_module_error!("pub fn main() -> Int() {}");
+}
+
+#[test]
+fn missing_type_constructor_arguments_in_type_annotation_2() {
+    assert_module_error!(
+        "pub fn main() {
+  let a: Int() = todo
+}"
+    );
+}
diff --git a/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_functions.snap b/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_functions.snap
new file mode 100644
index 000000000..a5005c54f
--- /dev/null
+++ b/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_functions.snap
@@ -0,0 +1,34 @@
+---
+source: compiler-core/src/type_/tests.rs
+expression: "\npub type Either(a, b) {\n  Left(value: a)\n  Right(value: b)\n}\n\nfn a_or_b(_first: value, second: value) -> value {\n  second\n}\n\npub fn main() {\n  let func = a_or_b(fn() { Left(1) }, fn() { Right(\"hello\") })\n  Left(..func(), value: 10)\n}\n"
+---
+----- SOURCE CODE
+
+pub type Either(a, b) {
+  Left(value: a)
+  Right(value: b)
+}
+
+fn a_or_b(_first: value, second: value) -> value {
+  second
+}
+
+pub fn main() {
+  let func = a_or_b(fn() { Left(1) }, fn() { Right("hello") })
+  Left(..func(), value: 10)
+}
+
+
+----- ERROR
+error: Unsafe record update
+   ┌─ /src/one/two.gleam:13:10
+   │
+13 │   Left(..func(), value: 10)
+   │          ^^^^^^ I'm not sure this is always a `Left`
+
+
+This value cannot be used to build an updated `Left` 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.
diff --git a/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_nested_type.snap b/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_nested_type.snap
new file mode 100644
index 000000000..81295aa6f
--- /dev/null
+++ b/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_nested_type.snap
@@ -0,0 +1,38 @@
+---
+source: compiler-core/src/type_/tests.rs
+expression: "\npub type Box(a) {\n  Box(inner: a)\n}\n\npub type Either(a, b) {\n  Left(value: a)\n  Right(value: b)\n}\n\nfn a_or_b(_first: value, second: value) -> value {\n  second\n}\n\npub fn main() {\n  let Box(inner) = a_or_b(Box(Left(1)), Box(Right(\"hello\")))\n  Left(..inner, value: 10)\n}\n"
+---
+----- SOURCE CODE
+
+pub type Box(a) {
+  Box(inner: a)
+}
+
+pub type Either(a, b) {
+  Left(value: a)
+  Right(value: b)
+}
+
+fn a_or_b(_first: value, second: value) -> value {
+  second
+}
+
+pub fn main() {
+  let Box(inner) = a_or_b(Box(Left(1)), Box(Right("hello")))
+  Left(..inner, value: 10)
+}
+
+
+----- ERROR
+error: Unsafe record update
+   ┌─ /src/one/two.gleam:17:10
+   │
+17 │   Left(..inner, value: 10)
+   │          ^^^^^ I'm not sure this is always a `Left`
+
+
+This value cannot be used to build an updated `Left` 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.
diff --git a/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_tuples.snap b/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_tuples.snap
new file mode 100644
index 000000000..4f53d544d
--- /dev/null
+++ b/compiler-core/src/type_/snapshots/gleam_core__type___tests__type_unification_removes_inferred_variant_in_tuples.snap
@@ -0,0 +1,34 @@
+---
+source: compiler-core/src/type_/tests.rs
+expression: "\npub type Either(a, b) {\n  Left(value: a)\n  Right(value: b)\n}\n\nfn a_or_b(_first: value, second: value) -> value {\n  second\n}\n\npub fn main() {\n  let #(right) = a_or_b(#(Left(5)), #(Right(\"hello\")))\n  Left(..right, value: 10)\n}\n"
+---
+----- SOURCE CODE
+
+pub type Either(a, b) {
+  Left(value: a)
+  Right(value: b)
+}
+
+fn a_or_b(_first: value, second: value) -> value {
+  second
+}
+
+pub fn main() {
+  let #(right) = a_or_b(#(Left(5)), #(Right("hello")))
+  Left(..right, value: 10)
+}
+
+
+----- ERROR
+error: Unsafe record update
+   ┌─ /src/one/two.gleam:13:10
+   │
+13 │   Left(..right, value: 10)
+   │          ^^^^^ I'm not sure this is always a `Left`
+
+
+This value cannot be used to build an updated `Left` 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.
diff --git a/compiler-core/src/type_/tests.rs b/compiler-core/src/type_/tests.rs
index 088c48c6d..58c99e21b 100644
--- a/compiler-core/src/type_/tests.rs
+++ b/compiler-core/src/type_/tests.rs
@@ -2142,6 +2142,76 @@ pub fn fun(x) {
     );
 }
 
+#[test]
+// https://github.com/gleam-lang/gleam/issues/3797
+fn type_unification_removes_inferred_variant_in_tuples() {
+    assert_module_error!(
+        r#"
+pub type Either(a, b) {
+  Left(value: a)
+  Right(value: b)
+}
+
+fn a_or_b(_first: value, second: value) -> value {
+  second
+}
+
+pub fn main() {
+  let #(right) = a_or_b(#(Left(5)), #(Right("hello")))
+  Left(..right, value: 10)
+}
+"#
+    );
+}
+
+#[test]
+// https://github.com/gleam-lang/gleam/issues/3797
+fn type_unification_removes_inferred_variant_in_functions() {
+    assert_module_error!(
+        r#"
+pub type Either(a, b) {
+  Left(value: a)
+  Right(value: b)
+}
+
+fn a_or_b(_first: value, second: value) -> value {
+  second
+}
+
+pub fn main() {
+  let func = a_or_b(fn() { Left(1) }, fn() { Right("hello") })
+  Left(..func(), value: 10)
+}
+"#
+    );
+}
+
+#[test]
+// https://github.com/gleam-lang/gleam/issues/3797
+fn type_unification_removes_inferred_variant_in_nested_type() {
+    assert_module_error!(
+        r#"
+pub type Box(a) {
+  Box(inner: a)
+}
+
+pub type Either(a, b) {
+  Left(value: a)
+  Right(value: b)
+}
+
+fn a_or_b(_first: value, second: value) -> value {
+  second
+}
+
+pub fn main() {
+  let Box(inner) = a_or_b(Box(Left(1)), Box(Right("hello")))
+  Left(..inner, value: 10)
+}
+"#
+    );
+}
+
 #[test]
 fn module_constants() {
     assert_module_infer!(