Repository: gleam-lang/gleam (rust)

Issue:
Inexhaustive warning message is incorrect for case expressions with multiple subjects
```
warning: Inexhaustive patterns
  ┌─ /src/warning/wrn.gleam:3:3
  │  
3 │ ╭   case x, y {
4 │ │     True, _ -> 1
5 │ │   }
  │ ╰───^

This case expression does not have a pattern for all possible values.
If is run on one of the values without a pattern then it will crash.

The missing patterns are:

    False
```

The missing pattern should be `False, _`. The fact there are multiple patterns has been lost.

Pull request:
Print correct missing patterns for inexhaustive case expressions with multiple subjects
Fixes #2426.
This ensures that a pattern is printed for every subject of the expression, in the correct order.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99ad3d284..41acb8a1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -299,6 +299,10 @@
 - Fixed a bug where the provided `REBAR_BARE_COMPILER_OUTPUT_DIR` env var would use relative path instead of absolute path causing compilation errors in some packages.
   ([Gustavo Inacio](https://github.com/gusinacio))
 
+- Fixed a bug where the compiler would print incorrect missing patterns for
+  inexhaustive case expressions matching on more than one subject.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ## v1.4.1 - 2024-08-04
 
 ### Bug Fixes
diff --git a/compiler-core/src/exhaustiveness.rs b/compiler-core/src/exhaustiveness.rs
index 2fb860388..b3c1ff6d9 100644
--- a/compiler-core/src/exhaustiveness.rs
+++ b/compiler-core/src/exhaustiveness.rs
@@ -221,6 +221,7 @@ pub struct Diagnostics {
 pub struct Match {
     pub tree: Decision,
     pub diagnostics: Diagnostics,
+    pub subject_variables: Vec<Variable>,
 }
 
 impl Match {
@@ -291,6 +292,7 @@ impl<'a> Compiler<'a> {
         Match {
             tree,
             diagnostics: self.diagnostics,
+            subject_variables: self.subject_variables,
         }
     }
 
diff --git a/compiler-core/src/exhaustiveness/missing_patterns.rs b/compiler-core/src/exhaustiveness/missing_patterns.rs
index c1037d09e..f0f527539 100644
--- a/compiler-core/src/exhaustiveness/missing_patterns.rs
+++ b/compiler-core/src/exhaustiveness/missing_patterns.rs
@@ -8,7 +8,13 @@ pub fn missing_patterns(matches: &Match, environment: &Environment<'_>) -> Vec<E
     let mut names = HashSet::new();
     let mut steps = Vec::new();
 
-    add_missing_patterns(&matches.tree, &mut steps, &mut names, environment);
+    add_missing_patterns(
+        &matches.tree,
+        &matches.subject_variables,
+        &mut steps,
+        &mut names,
+        environment,
+    );
 
     let mut missing: Vec<EcoString> = names.into_iter().collect();
 
@@ -58,16 +64,17 @@ impl Term {
 
 fn add_missing_patterns(
     node: &Decision,
+    subjects: &Vec<Variable>,
     terms: &mut Vec<Term>,
     missing: &mut HashSet<EcoString>,
     environment: &Environment<'_>,
 ) {
-    let mut printer = Printer::new(&environment.value_names);
     match node {
         Decision::Success(_) => {}
 
         Decision::Failure => {
             let mut mapping = HashMap::new();
+            let printer = Printer::new(&environment.value_names);
 
             // At this point the terms stack looks something like this:
             // `[term, term + arguments, term, ...]`. To construct a pattern
@@ -86,16 +93,13 @@ fn add_missing_patterns(
                 _ = mapping.insert(step.variable().id, index);
             }
 
-            let name = terms
-                .first()
-                .map(|term| printer.print_term(term, terms, &mapping))
-                .unwrap_or_else(|| "_".into());
+            let pattern = printer.print_terms(subjects, terms, &mapping);
 
-            _ = missing.insert(name);
+            _ = missing.insert(pattern);
         }
 
         Decision::Guard(_, _, fallback) => {
-            add_missing_patterns(fallback, terms, missing, environment);
+            add_missing_patterns(fallback, subjects, terms, missing, environment);
         }
 
         Decision::Switch(variable, cases, fallback) => {
@@ -139,12 +143,12 @@ fn add_missing_patterns(
                     }
                 }
 
-                add_missing_patterns(&case.body, terms, missing, environment);
+                add_missing_patterns(&case.body, subjects, terms, missing, environment);
                 _ = terms.pop();
             }
 
             if let Some(node) = fallback {
-                add_missing_patterns(node, terms, missing, environment);
+                add_missing_patterns(node, subjects, terms, missing, environment);
             }
         }
 
@@ -156,7 +160,7 @@ fn add_missing_patterns(
             terms.push(Term::EmptyList {
                 variable: variable.clone(),
             });
-            add_missing_patterns(empty, terms, missing, environment);
+            add_missing_patterns(empty, subjects, terms, missing, environment);
             _ = terms.pop();
 
             terms.push(Term::List {
@@ -164,7 +168,7 @@ fn add_missing_patterns(
                 first: non_empty.first.clone(),
                 rest: non_empty.rest.clone(),
             });
-            add_missing_patterns(&non_empty.decision, terms, missing, environment);
+            add_missing_patterns(&non_empty.decision, subjects, terms, missing, environment);
             _ = terms.pop();
         }
     }
diff --git a/compiler-core/src/exhaustiveness/printer.rs b/compiler-core/src/exhaustiveness/printer.rs
index 0b7a1c466..c4052582f 100644
--- a/compiler-core/src/exhaustiveness/printer.rs
+++ b/compiler-core/src/exhaustiveness/printer.rs
@@ -2,7 +2,7 @@ use std::collections::HashMap;
 
 use ecow::EcoString;
 
-use super::missing_patterns::Term;
+use super::{missing_patterns::Term, Variable};
 
 #[derive(Debug, Default)]
 pub struct ValueNames {
@@ -160,19 +160,31 @@ impl<'a> Printer<'a> {
         Printer { names }
     }
 
-    pub fn print_term(
-        &mut self,
-        term: &Term,
+    pub fn print_terms(
+        &self,
+        subjects: &[Variable],
         terms: &[Term],
         mapping: &HashMap<usize, usize>,
     ) -> EcoString {
         let mut buffer = EcoString::new();
-        self.print(term, terms, mapping, &mut buffer);
+        for (i, subject) in subjects.iter().enumerate() {
+            if i != 0 {
+                buffer.push_str(", ");
+            }
+
+            match mapping.get(&subject.id) {
+                Some(&index) => {
+                    let term = terms.get(index).expect("Term must exist");
+                    self.print(term, terms, mapping, &mut buffer);
+                }
+                None => buffer.push('_'),
+            }
+        }
         buffer
     }
 
     fn print(
-        &mut self,
+        &self,
         term: &Term,
         terms: &[Term],
         mapping: &HashMap<usize, usize>,
@@ -252,7 +264,7 @@ impl<'a> Printer<'a> {
     }
 
     fn print_list(
-        &mut self,
+        &self,
         term: &Term,
         terms: &[Term],
         mapping: &HashMap<usize, usize>,
@@ -311,13 +323,13 @@ mod tests {
         }
     }
 
-    fn get_terms_and_mapping(terms: Vec<Term>) -> (Vec<Term>, HashMap<usize, usize>) {
+    fn get_mapping(terms: &[Term]) -> HashMap<usize, usize> {
         let mut mapping: HashMap<usize, usize> = HashMap::new();
 
         for (index, term) in terms.iter().enumerate() {
             _ = mapping.insert(term.variable().id, index);
         }
-        (terms, mapping)
+        mapping
     }
 
     #[test]
@@ -326,19 +338,20 @@ mod tests {
 
         names.named_constructor_in_scope("module".into(), "Wibble".into(), "Wibble".into());
 
-        let mut printer = Printer::new(&mut names);
+        let printer = Printer::new(&names);
 
+        let subjects = &[make_variable(0)];
         let term = Term::Variant {
-            variable: make_variable(0),
+            variable: subjects[0].clone(),
             name: "Wibble".into(),
             module: "module".into(),
             arguments: Vec::new(),
         };
 
-        assert_eq!(
-            printer.print_term(&term, &Vec::new(), &HashMap::new()),
-            "Wibble"
-        );
+        let terms = &[term];
+        let mapping = get_mapping(terms);
+
+        assert_eq!(printer.print_terms(subjects, terms, &mapping), "Wibble");
     }
 
     #[test]
@@ -347,25 +360,31 @@ mod tests {
 
         names.named_constructor_in_scope("module".into(), "Wibble".into(), "Wibble".into());
 
-        let mut printer = Printer::new(&mut names);
+        let printer = Printer::new(&names);
 
         let var1 = make_variable(1);
 
         let var2 = make_variable(2);
 
+        let subjects = &[make_variable(0)];
         let term = Term::Variant {
-            variable: make_variable(0),
+            variable: subjects[0].clone(),
             name: "Wibble".into(),
             module: "module".into(),
             arguments: vec![var1.clone(), var2.clone()],
         };
 
-        let (terms, mapping) = get_terms_and_mapping(vec![
+        let terms = &[
+            term,
             Term::EmptyList { variable: var1 },
             Term::Infinite { variable: var2 },
-        ]);
+        ];
+        let mapping = get_mapping(terms);
 
-        assert_eq!(printer.print_term(&term, &terms, &mapping), "Wibble([], _)");
+        assert_eq!(
+            printer.print_terms(subjects, terms, &mapping),
+            "Wibble([], _)"
+        );
     }
 
     #[test]
@@ -374,17 +393,21 @@ mod tests {
 
         names.imported_module("mod".into(), "shapes".into());
 
-        let mut printer = Printer::new(&mut names);
+        let printer = Printer::new(&names);
 
+        let subjects = &[make_variable(0)];
         let term = Term::Variant {
-            variable: make_variable(0),
+            variable: subjects[0].clone(),
             name: "Rectangle".into(),
             module: "mod".into(),
             arguments: Vec::new(),
         };
 
+        let terms = &[term];
+        let mapping = get_mapping(terms);
+
         assert_eq!(
-            printer.print_term(&term, &Vec::new(), &HashMap::new()),
+            printer.print_terms(subjects, terms, &mapping),
             "shapes.Rectangle"
         );
     }
@@ -395,20 +418,22 @@ mod tests {
 
         names.named_constructor_in_scope("regex".into(), "Regex".into(), "Regex".into());
 
-        let mut printer = Printer::new(&mut names);
+        let printer = Printer::new(&names);
 
         let arg = make_variable(1);
 
+        let subjects = &[make_variable(0)];
         let term = Term::Variant {
-            variable: make_variable(0),
+            variable: subjects[0].clone(),
             name: "Regex".into(),
             module: "regex".into(),
             arguments: vec![arg.clone()],
         };
 
-        let (terms, mapping) = get_terms_and_mapping(vec![Term::Infinite { variable: arg }]);
+        let terms = &[term, Term::Infinite { variable: arg }];
+        let mapping = get_mapping(terms);
 
-        assert_eq!(printer.print_term(&term, &terms, &mapping), "Regex(_)");
+        assert_eq!(printer.print_terms(subjects, terms, &mapping), "Regex(_)");
     }
 
     #[test]
@@ -418,25 +443,30 @@ mod tests {
         names.named_constructor_in_scope("regex".into(), "Regex".into(), "Reg".into());
         names.named_constructor_in_scope("gleam".into(), "None".into(), "None".into());
 
-        let mut printer = Printer::new(&mut names);
+        let printer = Printer::new(&names);
 
         let arg = make_variable(1);
 
+        let subjects = &[make_variable(0)];
         let term = Term::Variant {
-            variable: make_variable(0),
+            variable: subjects[0].clone(),
             name: "Regex".into(),
             module: "regex".into(),
             arguments: vec![arg.clone()],
         };
 
-        let (terms, mapping) = get_terms_and_mapping(vec![Term::Variant {
-            variable: arg,
-            name: "None".into(),
-            module: "gleam".into(),
-            arguments: vec![],
-        }]);
+        let terms = &[
+            term,
+            Term::Variant {
+                variable: arg,
+                name: "None".into(),
+                module: "gleam".into(),
+                arguments: vec![],
+            },
+        ];
+        let mapping = get_mapping(terms);
 
-        assert_eq!(printer.print_term(&term, &terms, &mapping), "Reg(None)");
+        assert_eq!(printer.print_terms(subjects, terms, &mapping), "Reg(None)");
     }
 
     #[test]
@@ -445,19 +475,21 @@ mod tests {
 
         names.named_constructor_in_scope("module".into(), "Type".into(), "Type".into());
 
-        let mut printer = Printer::new(&mut names);
+        let printer = Printer::new(&names);
 
         let var1 = make_variable(1);
         let var2 = make_variable(2);
         let var3 = make_variable(3);
 
+        let subjects = &[make_variable(0)];
         let term = Term::List {
-            variable: make_variable(0),
+            variable: subjects[0].clone(),
             first: var1.clone(),
             rest: var2.clone(),
         };
 
-        let (terms, mapping) = get_terms_and_mapping(vec![
+        let terms = &[
+            term,
             Term::Variant {
                 variable: var1,
                 name: "Type".into(),
@@ -467,11 +499,48 @@ mod tests {
             Term::List {
                 variable: var2,
                 first: var3.clone(),
-                rest: make_variable(0),
+                rest: make_variable(4),
             },
             Term::Infinite { variable: var3 },
-        ]);
+        ];
+        let mapping = get_mapping(terms);
 
-        assert_eq!(printer.print_term(&term, &terms, &mapping), "[Type, _, ..]");
+        assert_eq!(
+            printer.print_terms(subjects, terms, &mapping),
+            "[Type, _, ..]"
+        );
+    }
+
+    #[test]
+    fn test_multi_pattern() {
+        let mut names = ValueNames::new();
+
+        names.named_constructor_in_scope("gleam".into(), "Ok".into(), "Ok".into());
+        names.named_constructor_in_scope("gleam".into(), "False".into(), "False".into());
+
+        let printer = Printer::new(&names);
+
+        let subjects = &[make_variable(0), make_variable(1), make_variable(2)];
+
+        let terms = &[
+            Term::Variant {
+                variable: subjects[0].clone(),
+                name: "Ok".into(),
+                module: "gleam".into(),
+                arguments: vec![make_variable(3)],
+            },
+            Term::Variant {
+                variable: subjects[2].clone(),
+                name: "False".into(),
+                module: "gleam".into(),
+                arguments: Vec::new(),
+            },
+        ];
+        let mapping = get_mapping(terms);
+
+        assert_eq!(
+            printer.print_terms(subjects, terms, &mapping),
+            "Ok(_), _, False"
+        );
     }
 }


Test patch:
diff --git a/compiler-core/src/type_/tests/exhaustiveness.rs b/compiler-core/src/type_/tests/exhaustiveness.rs
index 827306129..b36f28b7b 100644
--- a/compiler-core/src/type_/tests/exhaustiveness.rs
+++ b/compiler-core/src/type_/tests/exhaustiveness.rs
@@ -1196,3 +1196,81 @@ case name {}
 "#
     );
 }
+
+#[test]
+fn empty_case_of_multi_pattern() {
+    assert_error!(
+        "
+let a = Ok(1)
+let b = True
+case a, b {}
+"
+    );
+}
+
+#[test]
+fn inexhaustive_multi_pattern() {
+    assert_error!(
+        "
+let a = Ok(1)
+let b = True
+case a, b {
+  Error(_), _ -> Nil
+}
+"
+    );
+}
+
+#[test]
+fn inexhaustive_multi_pattern2() {
+    assert_error!(
+        "
+let a = Ok(1)
+let b = True
+case a, b {
+  Ok(1), True -> Nil
+}
+"
+    );
+}
+
+#[test]
+fn inexhaustive_multi_pattern3() {
+    assert_error!(
+        "
+let a = Ok(1)
+let b = True
+case a, b {
+  _, False -> Nil
+}
+"
+    );
+}
+
+#[test]
+fn inexhaustive_multi_pattern4() {
+    assert_error!(
+        "
+let a = 12
+let b = 3.14
+let c = False
+case a, b, c {
+  1, 2.0, True -> Nil
+}
+"
+    );
+}
+
+#[test]
+fn inexhaustive_multi_pattern5() {
+    assert_error!(
+        "
+let a = 12
+let b = 3.14
+let c = False
+case a, b, c {
+  12, _, False -> Nil
+}
+"
+    );
+}
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__empty_case_of_multi_pattern.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__empty_case_of_multi_pattern.snap
new file mode 100644
index 000000000..83803b7a5
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__empty_case_of_multi_pattern.snap
@@ -0,0 +1,17 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nlet a = Ok(1)\nlet b = True\ncase a, b {}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:1
+  │
+4 │ case a, b {}
+  │ ^^^^^^^^^^^^
+
+This case expression does not have a pattern for all possible values. If it
+is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    Error(_), _
+    Ok(_), _
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern.snap
new file mode 100644
index 000000000..9170d830c
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nlet a = Ok(1)\nlet b = True\ncase a, b {\n  Error(_), _ -> Nil\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:1
+  │  
+4 │ ╭ case a, b {
+5 │ │   Error(_), _ -> Nil
+6 │ │ }
+  │ ╰─^
+
+This case expression does not have a pattern for all possible values. If it
+is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    Ok(_), _
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern2.snap
new file mode 100644
index 000000000..80199c255
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern2.snap
@@ -0,0 +1,20 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nlet a = Ok(1)\nlet b = True\ncase a, b {\n  Ok(1), True -> Nil\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:1
+  │  
+4 │ ╭ case a, b {
+5 │ │   Ok(1), True -> Nil
+6 │ │ }
+  │ ╰─^
+
+This case expression does not have a pattern for all possible values. If it
+is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    Error(_), True
+    Ok(_), True
+    _, False
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern3.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern3.snap
new file mode 100644
index 000000000..dcb8b1c08
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern3.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nlet a = Ok(1)\nlet b = True\ncase a, b {\n  _, False -> Nil\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:1
+  │  
+4 │ ╭ case a, b {
+5 │ │   _, False -> Nil
+6 │ │ }
+  │ ╰─^
+
+This case expression does not have a pattern for all possible values. If it
+is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    _, True
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern4.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern4.snap
new file mode 100644
index 000000000..d04d3f889
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern4.snap
@@ -0,0 +1,19 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nlet a = 12\nlet b = 3.14\nlet c = False\ncase a, b, c {\n  1, 2.0, True -> Nil\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:5:1
+  │  
+5 │ ╭ case a, b, c {
+6 │ │   1, 2.0, True -> Nil
+7 │ │ }
+  │ ╰─^
+
+This case expression does not have a pattern for all possible values. If it
+is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    _, _, False
+    _, _, True
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern5.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern5.snap
new file mode 100644
index 000000000..4b702c19d
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__inexhaustive_multi_pattern5.snap
@@ -0,0 +1,19 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nlet a = 12\nlet b = 3.14\nlet c = False\ncase a, b, c {\n  12, _, False -> Nil\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:5:1
+  │  
+5 │ ╭ case a, b, c {
+6 │ │   12, _, False -> Nil
+7 │ │ }
+  │ ╰─^
+
+This case expression does not have a pattern for all possible values. If it
+is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    _, _, False
+    _, _, True