Repository: gleam-lang/gleam (rust)

Issue:
Suggest unwrapping when passing `Result(a, _)` instead of `a`
It would be nice if the compiler could suggest how to deal with a result when it's being used where the wrapped `Ok` value is expected instead. What I mean is:
```gleam
pub fn main() {
  let response =
    request.new()
    |> request.set_host("pokeapi.co")
    |> request.set_path("api/v2/pokemon/ditto")
    |> httpc.send

  parse_response(response)
}

fn parse_response(response: Response(String)) {
  todo
}
```
Have an error that looks like this:
```txt
error: Type mismatch
   ┌─ /Users/giacomocavalieri/Desktop/goto/src/goto.gleam:13:18
   │
13 │   parse_response(response)
   │                  ^^^^^^^^

Expected type:

    Response(String)

Found type:

    Result(Response(String), Dynamic)

Hint: to get a value out of a `Result` you can pattern match on it

    case response {
      Ok(response) -> parse_response(response)
      Error(_) -> todo as "deal with error"
    }
```
We could also have a code action to apply this fix too.

Pull request:
Suggest to pattern match on a `Result(a, _)` when `a` is expected
This PR closes #3644

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de36b777e..9465c8862 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,6 +47,47 @@
 
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- The compiler can now suggest to pattern match on a `Result(a, b)` if it's
+  being used where a value of type `a` is expected. For example, this code:
+
+  ```gleam
+  import gleam/list
+  import gleam/int
+
+  pub fn main() {
+    let not_a_number = list.first([1, 2, 3])
+    int.add(1, not_a_number)
+  }
+  ```
+
+  Results in the following error:
+
+  ```txt
+  error: Type mismatch
+    ┌─ /src/one/two.gleam:6:9
+    │
+  6 │   int.add(1, not_a_number)
+    │              ^^^^^^^^^^^^
+
+  Expected type:
+
+      Int
+
+  Found type:
+
+      Result(Int, a)
+
+  Hint: If you want to get a `Int` out of a `Result(Int, a)` you can pattern
+  match on it:
+
+      case result {
+        Ok(value) -> todo
+        Error(error) -> todo
+      }
+  ```
+
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Formatter
 
 ### Language Server
diff --git a/Cargo.lock b/Cargo.lock
index 375a3b89f..84b404f55 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1646,9 +1646,9 @@ dependencies = [
 
 [[package]]
 name = "pulldown-cmark"
-version = "0.11.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0"
+checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c"
 dependencies = [
  "bitflags 2.5.0",
  "memchr",
@@ -1778,9 +1778,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.10.5"
+version = "1.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
+checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1822,9 +1822,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
 
 [[package]]
 name = "reqwest"
-version = "0.12.7"
+version = "0.12.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
+checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
 dependencies = [
  "base64 0.22.0",
  "bytes",
@@ -2039,11 +2039,12 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.116"
+version = "1.0.128"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
 dependencies = [
  "itoa",
+ "memchr",
  "ryu",
  "serde",
 ]
diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs
index 355bf949e..4a5340dd8 100644
--- a/compiler-core/src/error.rs
+++ b/compiler-core/src/error.rs
@@ -1,6 +1,7 @@
 #![allow(clippy::unwrap_used, clippy::expect_used)]
 use crate::build::{Outcome, Runtime, Target};
 use crate::diagnostic::{Diagnostic, ExtraLabel, Label, Location};
+use crate::type_::collapse_links;
 use crate::type_::error::{MissingAnnotation, UnknownTypeHint};
 use crate::type_::error::{Named, RecordVariants};
 use crate::type_::printer::{Names, Printer};
@@ -19,6 +20,7 @@ use std::env;
 use std::fmt::{Debug, Display};
 use std::io::Write;
 use std::path::PathBuf;
+use std::sync::Arc;
 use termcolor::Buffer;
 use thiserror::Error;
 use vec1::Vec1;
@@ -1746,6 +1748,7 @@ But function expects:
                     situation,
                 } => {
                     let mut printer = Printer::new(names);
+                    let hint = hint_unwrap_result(expected, given, &mut printer);
                     let mut text = if let Some(description) = situation.as_ref().and_then(|s| s.description()) {
                         let mut text = description.to_string();
                         text.push('\n');
@@ -1758,10 +1761,13 @@ But function expects:
                     text.push_str(&printer.print_type(expected));
                     text.push_str("\n\nFound type:\n\n    ");
                     text.push_str(&printer.print_type(given));
+                    if hint.is_some() {
+                        text.push('\n');
+                    }
                     Diagnostic {
                         title: "Type mismatch".into(),
                         text,
-                        hint: None,
+                        hint,
                         level: Level::Error,
                         location: Some(Location {
                             label: Label {
@@ -3642,6 +3648,31 @@ fn hint_alternative_operator(op: &BinOp, given: &Type) -> Option<String> {
     }
 }
 
+fn hint_unwrap_result(
+    expected: &Arc<Type>,
+    given: &Arc<Type>,
+    printer: &mut Printer<'_>,
+) -> Option<String> {
+    // If the got type is `Result(a, _)` and the expected one is
+    // `a` then we can display the hint.
+    let wrapped_type = given.result_ok_type()?;
+    let expected = collapse_links(expected.clone());
+    if collapse_links(wrapped_type) != expected {
+        None
+    } else {
+        Some(wrap_format!(
+            "If you want to get a `{}` out of a `{}` you can pattern match on it:
+
+    case result {{
+      Ok(value) -> todo
+      Error(error) -> todo
+    }}",
+            printer.print_type(&expected),
+            printer.print_type(given),
+        ))
+    }
+}
+
 fn hint_numeric_message(alt: &str, type_: &str) -> String {
     format!("the {alt} operator can be used with {type_}s\n")
 }
diff --git a/compiler-core/src/type_.rs b/compiler-core/src/type_.rs
index 26c6fdc44..38b3369e0 100644
--- a/compiler-core/src/type_.rs
+++ b/compiler-core/src/type_.rs
@@ -100,6 +100,16 @@ impl Type {
         }
     }
 
+    pub fn result_ok_type(&self) -> Option<Arc<Type>> {
+        match self {
+            Self::Named {
+                module, name, args, ..
+            } if "Result" == name && is_prelude_module(module) => args.first().cloned(),
+            Self::Var { type_ } => type_.borrow().result_ok_type(),
+            Self::Named { .. } | Self::Tuple { .. } | Type::Fn { .. } => None,
+        }
+    }
+
     pub fn is_unbound(&self) -> bool {
         match self {
             Self::Var { type_ } => type_.borrow().is_unbound(),
@@ -837,6 +847,13 @@ impl TypeVar {
         }
     }
 
+    pub fn result_ok_type(&self) -> Option<Arc<Type>> {
+        match self {
+            TypeVar::Link { type_ } => type_.result_ok_type(),
+            TypeVar::Unbound { .. } | TypeVar::Generic { .. } => None,
+        }
+    }
+
     pub fn fn_types(&self) -> Option<(Vec<Arc<Type>>, Arc<Type>)> {
         match self {
             Self::Link { type_ } => type_.fn_types(),


Test patch:
diff --git a/compiler-core/src/type_/tests/errors.rs b/compiler-core/src/type_/tests/errors.rs
index 933c917d5..164caa248 100644
--- a/compiler-core/src/type_/tests/errors.rs
+++ b/compiler-core/src/type_/tests/errors.rs
@@ -2391,3 +2391,17 @@ pub fn main() {
 "
     );
 }
+
+#[test]
+fn suggest_unwrapping_a_result_when_types_match() {
+    assert_module_error!(
+        "
+pub fn main() {
+  let value = Ok(1)
+  add_1(value)
+}
+
+fn add_1(to x) { x + 1 }
+"
+    );
+}
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__suggest_unwrapping_a_result_when_types_match.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__suggest_unwrapping_a_result_when_types_match.snap
new file mode 100644
index 000000000..181e22faa
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__suggest_unwrapping_a_result_when_types_match.snap
@@ -0,0 +1,25 @@
+---
+source: compiler-core/src/type_/tests/errors.rs
+expression: "\npub fn main() {\n  let value = Ok(1)\n  add_1(value)\n}\n\nfn add_1(to x) { x + 1 }\n"
+---
+error: Type mismatch
+  ┌─ /src/one/two.gleam:4:9
+  │
+4 │   add_1(value)
+  │         ^^^^^
+
+Expected type:
+
+    Int
+
+Found type:
+
+    Result(Int, a)
+
+Hint: If you want to get a `Int` out of a `Result(Int, a)` you can pattern match
+on it:
+
+    case result {
+      Ok(value) -> todo
+      Error(error) -> todo
+    }