Repository: gleam-lang/gleam (rust)

Issue:
Analysis of lists stops at the first error
If I write `[1, "a", 1 + "a"]` the compiler only show an error for the first `"a"` but won't show an error for the second expression trying to sum an int and a string. I think we'd have to make analysis fault tolerant for lists in order to enable this.

Pull request:
Make analysis of lists, tuples, negation operators, `panic`, `echo` and `todo` fault tolerant
This PR closes #4482 and also improves fault tolerance for tuples, negation operators (`-` and `!`), `panic`, `echo` and `todo`

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6afd3d67..adc96199a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,6 +66,13 @@
       Person(name:, age:)
   ```
 
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
+- The analysis of lists, tuples, negation operators, `panic`, `echo` and `todo`
+  is now fault tolerant, meaning that the compiler will not stop reporting
+  errors as soon as it finds one.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Build tool
 
 - The build tool now supports placing modules in a directory called `dev`,
@@ -161,3 +168,7 @@
 - Fixed a bug where `case` expressions in custom panic messages would compile
   to invalid syntax on the JavaScript target.
   ([Surya Rose](https://github.com/GearsDatapacks))
+
+- Fixed a bug where an underscore after a zero in a number would compile to
+  invalid syntax on the JavaScript target.
+  ([Surya Rose](https://github.com/GearsDatapacks))
diff --git a/compiler-core/src/javascript/expression.rs b/compiler-core/src/javascript/expression.rs
index 3cf6b771f..8e4e4390d 100644
--- a/compiler-core/src/javascript/expression.rs
+++ b/compiler-core/src/javascript/expression.rs
@@ -1466,14 +1466,16 @@ pub fn int(value: &str) -> Document<'_> {
         value
     };
 
-    // If the number starts with `0x_`, `0b_` or `0o_`, that is valid Gleam syntax
-    // but not valid JavaScript syntax, so we remove the `_` here.
-    let value = value.trim_start_matches('_');
-
     let value = value.trim_start_matches('0');
     if value.is_empty() {
         out.push('0');
     }
+
+    // If the number starts with a `0` then an underscore, the `0` will be stripped,
+    // leaving the number to look something like `_1_2_3`, which is not valid syntax.
+    // Therefore, we strip the `_` to avoid this case.
+    let value = value.trim_start_matches('_');
+
     out.push_str(value);
 
     out.to_doc()
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index 3fd8bda45..e24bfca6a 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -287,22 +287,22 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
                 message: label,
                 kind,
                 ..
-            } => self.infer_todo(location, kind, label),
+            } => Ok(self.infer_todo(location, kind, label)),
 
             // A placeholder is used when the author has not provided a function
             // body, instead only giving an external implementation for this
             // target. This placeholder implementation will never be used so we
             // treat it as a `panic` expression during analysis.
-            UntypedExpr::Placeholder { location } => self.infer_panic(location, None),
+            UntypedExpr::Placeholder { location } => Ok(self.infer_panic(location, None)),
 
             UntypedExpr::Panic {
                 location, message, ..
-            } => self.infer_panic(location, message),
+            } => Ok(self.infer_panic(location, message)),
 
             UntypedExpr::Echo {
                 location,
                 expression,
-            } => self.infer_echo(location, expression),
+            } => Ok(self.infer_echo(location, expression)),
 
             UntypedExpr::Var { location, name, .. } => {
                 self.infer_var(name, location, ReferenceRegistration::RegisterReferences)
@@ -330,7 +330,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
 
             UntypedExpr::Tuple {
                 location, elements, ..
-            } => self.infer_tuple(elements, location),
+            } => Ok(self.infer_tuple(elements, location)),
 
             UntypedExpr::Float {
                 location, value, ..
@@ -370,7 +370,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
                 location,
                 elements,
                 tail,
-            } => self.infer_list(elements, tail, location),
+            } => Ok(self.infer_list(elements, tail, location)),
 
             UntypedExpr::Call {
                 location,
@@ -418,9 +418,13 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
                 arguments: args,
             } => self.infer_record_update(*constructor, record, args, location),
 
-            UntypedExpr::NegateBool { location, value } => self.infer_negate_bool(location, *value),
+            UntypedExpr::NegateBool { location, value } => {
+                Ok(self.infer_negate_bool(location, *value))
+            }
 
-            UntypedExpr::NegateInt { location, value } => self.infer_negate_int(location, *value),
+            UntypedExpr::NegateInt { location, value } => {
+                Ok(self.infer_negate_int(location, *value))
+            }
         }
     }
 
@@ -433,7 +437,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         location: SrcSpan,
         kind: TodoKind,
         message: Option<Box<UntypedExpr>>,
-    ) -> Result<TypedExpr, Error> {
+    ) -> TypedExpr {
         // Type the todo as whatever it would need to be to type check.
         let type_ = self.new_unbound_var();
 
@@ -448,65 +452,61 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
             type_: type_.clone(),
         });
 
-        let message = message
-            .map(|message| {
-                // If there is a message expression then it must be a string.
-                let message = self.infer(*message)?;
-                unify(string(), message.type_())
-                    .map_err(|e| convert_unify_error(e, message.location()))?;
-                Ok(Box::new(message))
-            })
-            .transpose()?;
+        let message = message.map(|message| {
+            // If there is a message expression then it must be a string.
+            let message = self.infer_no_error(*message);
+            if let Err(error) = unify(string(), message.type_()) {
+                self.problems
+                    .error(convert_unify_error(error, message.location()));
+            }
+            Box::new(message)
+        });
 
-        Ok(TypedExpr::Todo {
+        TypedExpr::Todo {
             location,
             type_,
             message,
             kind,
-        })
+        }
     }
 
-    fn infer_panic(
-        &mut self,
-        location: SrcSpan,
-        message: Option<Box<UntypedExpr>>,
-    ) -> Result<TypedExpr, Error> {
+    fn infer_panic(&mut self, location: SrcSpan, message: Option<Box<UntypedExpr>>) -> TypedExpr {
         let type_ = self.new_unbound_var();
         let message = match message {
+            None => None,
             Some(message) => {
-                let message = self.infer(*message)?;
-                unify(string(), message.type_())
-                    .map_err(|e| convert_unify_error(e, message.location()))?;
+                let message = self.infer_no_error(*message);
+                if let Err(error) = unify(string(), message.type_()) {
+                    self.problems
+                        .error(convert_unify_error(error, message.location()))
+                }
                 Some(Box::new(message))
             }
-            None => None,
         };
         self.previous_panics = true;
-        Ok(TypedExpr::Panic {
+        TypedExpr::Panic {
             location,
             type_,
             message,
-        })
+        }
     }
 
-    fn infer_echo(
-        &mut self,
-        location: SrcSpan,
-        expression: Option<Box<UntypedExpr>>,
-    ) -> Result<TypedExpr, Error> {
+    fn infer_echo(&mut self, location: SrcSpan, expression: Option<Box<UntypedExpr>>) -> TypedExpr {
         self.environment.echo_found = true;
         if let Some(expression) = expression {
-            let expression = self.infer(*expression)?;
+            let expression = self.infer_no_error(*expression);
             if self.previous_panics {
                 self.warn_for_unreachable_code(location, PanicPosition::EchoExpression);
             }
-            Ok(TypedExpr::Echo {
+            TypedExpr::Echo {
                 location,
                 type_: expression.type_(),
                 expression: Some(Box::new(expression)),
-            })
+            }
         } else {
-            Err(Error::EchoWithNoFollowingExpression { location })
+            self.problems
+                .error(Error::EchoWithNoFollowingExpression { location });
+            self.error_expr(location)
         }
     }
 
@@ -729,34 +729,32 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         })
     }
 
-    fn infer_negate_bool(
-        &mut self,
-        location: SrcSpan,
-        value: UntypedExpr,
-    ) -> Result<TypedExpr, Error> {
-        let value = self.infer(value)?;
+    fn infer_negate_bool(&mut self, location: SrcSpan, value: UntypedExpr) -> TypedExpr {
+        let value = self.infer_no_error(value);
 
-        unify(bool(), value.type_()).map_err(|e| convert_unify_error(e, value.location()))?;
+        if let Err(error) = unify(bool(), value.type_()) {
+            self.problems
+                .error(convert_unify_error(error, value.location()))
+        }
 
         if let TypedExpr::NegateBool { .. } = value {
             self.problems
                 .warning(Warning::UnnecessaryDoubleBoolNegation { location });
         }
 
-        Ok(TypedExpr::NegateBool {
+        TypedExpr::NegateBool {
             location,
             value: Box::new(value),
-        })
+        }
     }
 
-    fn infer_negate_int(
-        &mut self,
-        location: SrcSpan,
-        value: UntypedExpr,
-    ) -> Result<TypedExpr, Error> {
-        let value = self.infer(value)?;
+    fn infer_negate_int(&mut self, location: SrcSpan, value: UntypedExpr) -> TypedExpr {
+        let value = self.infer_no_error(value);
 
-        unify(int(), value.type_()).map_err(|e| convert_unify_error(e, value.location()))?;
+        if let Err(error) = unify(int(), value.type_()) {
+            self.problems
+                .error(convert_unify_error(error, value.location()));
+        }
 
         if let TypedExpr::Int { value: ref v, .. } = value {
             if v.starts_with('-') {
@@ -770,10 +768,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
                 .warning(Warning::UnnecessaryDoubleIntNegation { location });
         }
 
-        Ok(TypedExpr::NegateInt {
+        TypedExpr::NegateInt {
             location,
             value: Box::new(value),
-        })
+        }
     }
 
     fn infer_fn(
@@ -892,55 +890,56 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         elements: Vec<UntypedExpr>,
         tail: Option<Box<UntypedExpr>>,
         location: SrcSpan,
-    ) -> Result<TypedExpr, Error> {
+    ) -> TypedExpr {
         let type_ = self.new_unbound_var();
         // Type check each elements
-        let elements = elements
-            .into_iter()
-            .map(|element| {
-                let element = self.infer(element)?;
-                // Ensure they all have the same type
-                unify(type_.clone(), element.type_()).map_err(|e| {
-                    convert_unify_error(e.list_element_mismatch(), element.location())
-                })?;
-                Ok(element)
-            })
-            .try_collect()?;
+        let mut inferred_elements = Vec::with_capacity(elements.len());
+        for element in elements {
+            let element = self.infer_no_error(element);
+            if let Err(error) = unify(type_.clone(), element.type_()) {
+                self.problems.error(convert_unify_error(
+                    error.list_element_mismatch(),
+                    element.location(),
+                ))
+            };
+            inferred_elements.push(element);
+        }
+
         // Type check the ..tail, if there is one
         let type_ = list(type_);
         let tail = match tail {
             Some(tail) => {
-                let tail = self.infer(*tail)?;
+                let tail = self.infer_no_error(*tail);
                 // Ensure the tail has the same type as the preceding elements
-                unify(type_.clone(), tail.type_())
-                    .map_err(|e| convert_unify_error(e.list_tail_mismatch(), tail.location()))?;
+                if let Err(error) = unify(type_.clone(), tail.type_()) {
+                    self.problems.error(convert_unify_error(
+                        error.list_tail_mismatch(),
+                        tail.location(),
+                    ))
+                }
                 Some(Box::new(tail))
             }
             None => None,
         };
-        Ok(TypedExpr::List {
+        TypedExpr::List {
             location,
             type_,
-            elements,
+            elements: inferred_elements,
             tail,
-        })
+        }
     }
 
-    fn infer_tuple(
-        &mut self,
-        elements: Vec<UntypedExpr>,
-        location: SrcSpan,
-    ) -> Result<TypedExpr, Error> {
-        let elements: Vec<_> = elements
+    fn infer_tuple(&mut self, elements: Vec<UntypedExpr>, location: SrcSpan) -> TypedExpr {
+        let elements = elements
             .into_iter()
-            .map(|element| self.infer(element))
-            .try_collect()?;
-        let type_ = tuple(elements.iter().map(HasType::type_).collect());
-        Ok(TypedExpr::Tuple {
+            .map(|element| self.infer_no_error(element))
+            .collect_vec();
+        let type_ = tuple(elements.iter().map(HasType::type_).collect_vec());
+        TypedExpr::Tuple {
             location,
             elements,
             type_,
-        })
+        }
     }
 
     fn infer_var(


Test patch:
diff --git a/compiler-core/src/javascript/tests/numbers.rs b/compiler-core/src/javascript/tests/numbers.rs
index 5176cfaf2..5a88ba090 100644
--- a/compiler-core/src/javascript/tests/numbers.rs
+++ b/compiler-core/src/javascript/tests/numbers.rs
@@ -349,3 +349,84 @@ pub fn main() {
 "
     );
 }
+
+// https://github.com/gleam-lang/gleam/issues/4481
+#[test]
+fn underscore_after_zero_after_hex_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0x0_1_2_3
+}
+"
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4481
+#[test]
+fn underscore_after_zero_after_octal_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0o0_1_2_3
+}
+"
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4481
+#[test]
+fn underscore_after_zero_after_binary_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0b0_1_0_1
+}
+"
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4481
+#[test]
+fn underscore_after_zero() {
+    assert_js!(
+        "
+pub fn main() {
+  0_1_2_3
+}
+"
+    );
+}
+
+#[test]
+fn zero_after_underscore_after_hex_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0x_0_1_2_3
+}
+"
+    );
+}
+
+#[test]
+fn zero_after_underscore_after_octal_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0o_0_1_2_3
+}
+"
+    );
+}
+
+#[test]
+fn zero_after_underscore_after_binary_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0b_0_1_0_1
+}
+"
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero.snap
new file mode 100644
index 000000000..21934a13f
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0_1_2_3\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0_1_2_3
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 1_2_3;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_binary_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_binary_prefix.snap
new file mode 100644
index 000000000..92c0d0c50
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_binary_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0b0_1_0_1\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0b0_1_0_1
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0b1_0_1;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_hex_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_hex_prefix.snap
new file mode 100644
index 000000000..07943c0b2
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_hex_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0x0_1_2_3\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0x0_1_2_3
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0x1_2_3;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_octal_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_octal_prefix.snap
new file mode 100644
index 000000000..3e6461e66
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_zero_after_octal_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0o0_1_2_3\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0o0_1_2_3
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0o1_2_3;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_binary_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_binary_prefix.snap
new file mode 100644
index 000000000..50c7eaec8
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_binary_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0b_0_1_0_1\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0b_0_1_0_1
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0b0_1_0_1;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_hex_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_hex_prefix.snap
new file mode 100644
index 000000000..1bf69a0b4
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_hex_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0x_0_1_2_3\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0x_0_1_2_3
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0x0_1_2_3;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_octal_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_octal_prefix.snap
new file mode 100644
index 000000000..6e809444b
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__zero_after_underscore_after_octal_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0o_0_1_2_3\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0o_0_1_2_3
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0o0_1_2_3;
+}
diff --git a/compiler-core/src/type_/tests/errors.rs b/compiler-core/src/type_/tests/errors.rs
index 01406edff..d1589a31c 100644
--- a/compiler-core/src/type_/tests/errors.rs
+++ b/compiler-core/src/type_/tests/errors.rs
@@ -3016,3 +3016,58 @@ fn int_operator_on_floats_2() {
 fn add_on_strings() {
     assert_error!(r#""Hello, " + "Jak""#);
 }
+
+#[test]
+fn fault_tolerant_list() {
+    assert_module_error!(
+        r#"
+pub fn main() {
+  [1, "a", 1.0, "a" + 1]
+}
+"#
+    );
+}
+
+#[test]
+fn fault_tolerant_list_tail() {
+    assert_module_error!(
+        r#"
+pub fn main() {
+  [1, "a", ..["a", "b"]]
+}
+"#
+    );
+}
+
+#[test]
+fn fault_tolerant_negate_bool() {
+    assert_module_error!(
+        r#"
+pub fn main() {
+  !!{ True || a }
+}
+"#
+    );
+}
+
+#[test]
+fn fault_tolerant_negate_int() {
+    assert_module_error!(
+        r#"
+pub fn main() {
+  --{ 1 + a }
+}
+"#
+    );
+}
+
+#[test]
+fn fault_tolerant_tuple() {
+    assert_module_error!(
+        r#"
+pub fn main() {
+  #(1, 1 + "a", not_in_scope)
+}
+"#
+    );
+}
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_list.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_list.snap
new file mode 100644
index 000000000..d31a8ddd7
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_list.snap
@@ -0,0 +1,61 @@
+---
+source: compiler-core/src/type_/tests/errors.rs
+expression: "\npub fn main() {\n  [1, \"a\", 1.0, \"a\" + 1]\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  [1, "a", 1.0, "a" + 1]
+}
+
+
+----- ERROR
+error: Type mismatch
+  ┌─ /src/one/two.gleam:3:7
+  │
+3 │   [1, "a", 1.0, "a" + 1]
+  │       ^^^
+
+All elements of a list must be the same type, but this one doesn't
+match the one before it.
+
+Expected type:
+
+    Int
+
+Found type:
+
+    String
+
+error: Type mismatch
+  ┌─ /src/one/two.gleam:3:12
+  │
+3 │   [1, "a", 1.0, "a" + 1]
+  │            ^^^
+
+All elements of a list must be the same type, but this one doesn't
+match the one before it.
+
+Expected type:
+
+    Int
+
+Found type:
+
+    Float
+
+error: Type mismatch
+  ┌─ /src/one/two.gleam:3:17
+  │
+3 │   [1, "a", 1.0, "a" + 1]
+  │                 ^^^
+
+The + operator expects arguments of this type:
+
+    Int
+
+But this argument has this type:
+
+    String
+
+Hint: Strings can be joined using the `<>` operator.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_list_tail.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_list_tail.snap
new file mode 100644
index 000000000..1e142e7c2
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_list_tail.snap
@@ -0,0 +1,45 @@
+---
+source: compiler-core/src/type_/tests/errors.rs
+expression: "\npub fn main() {\n  [1, \"a\", ..[\"a\", \"b\"]]\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  [1, "a", ..["a", "b"]]
+}
+
+
+----- ERROR
+error: Type mismatch
+  ┌─ /src/one/two.gleam:3:7
+  │
+3 │   [1, "a", ..["a", "b"]]
+  │       ^^^
+
+All elements of a list must be the same type, but this one doesn't
+match the one before it.
+
+Expected type:
+
+    Int
+
+Found type:
+
+    String
+
+error: Type mismatch
+  ┌─ /src/one/two.gleam:3:14
+  │
+3 │   [1, "a", ..["a", "b"]]
+  │              ^^^^^^^^^^
+
+All elements in a list must have the same type, but the elements of
+this list don't match the type of the elements being prepended to it.
+
+Expected type:
+
+    List(Int)
+
+Found type:
+
+    List(String)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_negate_bool.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_negate_bool.snap
new file mode 100644
index 000000000..1cf584496
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_negate_bool.snap
@@ -0,0 +1,19 @@
+---
+source: compiler-core/src/type_/tests/errors.rs
+expression: "\npub fn main() {\n  !!{ True || a }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  !!{ True || a }
+}
+
+
+----- ERROR
+error: Unknown variable
+  ┌─ /src/one/two.gleam:3:15
+  │
+3 │   !!{ True || a }
+  │               ^
+
+The name `a` is not in scope here.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_negate_int.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_negate_int.snap
new file mode 100644
index 000000000..8cda46bbc
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_negate_int.snap
@@ -0,0 +1,19 @@
+---
+source: compiler-core/src/type_/tests/errors.rs
+expression: "\npub fn main() {\n  --{ 1 + a }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  --{ 1 + a }
+}
+
+
+----- ERROR
+error: Unknown variable
+  ┌─ /src/one/two.gleam:3:11
+  │
+3 │   --{ 1 + a }
+  │           ^
+
+The name `a` is not in scope here.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_tuple.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_tuple.snap
new file mode 100644
index 000000000..963815752
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__fault_tolerant_tuple.snap
@@ -0,0 +1,35 @@
+---
+source: compiler-core/src/type_/tests/errors.rs
+expression: "\npub fn main() {\n  #(1, 1 + \"a\", not_in_scope)\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  #(1, 1 + "a", not_in_scope)
+}
+
+
+----- ERROR
+error: Type mismatch
+  ┌─ /src/one/two.gleam:3:12
+  │
+3 │   #(1, 1 + "a", not_in_scope)
+  │            ^^^
+
+The + operator expects arguments of this type:
+
+    Int
+
+But this argument has this type:
+
+    String
+
+Hint: Strings can be joined using the `<>` operator.
+
+error: Unknown variable
+  ┌─ /src/one/two.gleam:3:17
+  │
+3 │   #(1, 1 + "a", not_in_scope)
+  │                 ^^^^^^^^^^^^
+
+The name `not_in_scope` is not in scope here.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__negative_out_of_range_erlang_float_in_pattern.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__negative_out_of_range_erlang_float_in_pattern.snap
index 3b0c7aeba..6d9ef653f 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__negative_out_of_range_erlang_float_in_pattern.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__negative_out_of_range_erlang_float_in_pattern.snap
@@ -14,6 +14,14 @@ error: Unknown variable
 
 The name `x` is not in scope here.
 
+error: Unknown variable
+  ┌─ /src/one/two.gleam:1:32
+  │
+1 │ let assert [-1.8e308, b] = [x, y]
+  │                                ^
+
+The name `y` is not in scope here.
+
 error: Float is outside Erlang's floating point range
   ┌─ /src/one/two.gleam:1:13
   │
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__out_of_range_erlang_float_in_pattern.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__out_of_range_erlang_float_in_pattern.snap
index a35d4682d..8c9dfe820 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__out_of_range_erlang_float_in_pattern.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__out_of_range_erlang_float_in_pattern.snap
@@ -14,6 +14,14 @@ error: Unknown variable
 
 The name `x` is not in scope here.
 
+error: Unknown variable
+  ┌─ /src/one/two.gleam:1:31
+  │
+1 │ let assert [1.8e308, b] = [x, y]
+  │                               ^
+
+The name `y` is not in scope here.
+
 error: Float is outside Erlang's floating point range
   ┌─ /src/one/two.gleam:1:13
   │
diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__opaque_type_destructure.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__opaque_type_destructure.snap
index dc9e0ce01..f5b041bb2 100644
--- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__opaque_type_destructure.snap
+++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__opaque_type_destructure.snap
@@ -17,3 +17,11 @@ error: Unknown variable
   │     ^
 
 The name `name` is not in scope here.
+
+error: Unknown variable
+  ┌─ src/two.gleam:8:11
+  │
+8 │   #(name, score)
+  │           ^
+
+The name `score` is not in scope here.