Repository: gleam-lang/gleam (rust)

Issue:
Compiler crash when compiling specific pattern on javascript
For some reason, when compiling a bit array pattern inside a tuple pattern on the javascript target, the compiler will crash. For example, this code:
```gleam
pub fn main() {
  let assert #(<<>>) = todo
}
```
Causes a panic in the code generation module for patterns on JavaScript. This code compiles and runs fine on Erlang.
Discord thread where this was discussed: https://discord.com/channels/768594524158427167/1377007726017642719

Pull request description:
Fix code generation of nested bit array patterns on JavaScript
Fixes #4630

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6008906f8..87fbf7a4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -60,6 +60,11 @@
   produced if the completion was followed.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- Fixed a bug where the compiler would crash when compiling `let assert`
+  statements which contained a bit array pattern inside a tuple pattern on the
+  JavaScript target.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ## v1.11.0-rc1 - 2025-05-15
 
 ### Compiler
diff --git a/compiler-core/src/javascript/decision.rs b/compiler-core/src/javascript/decision.rs
index 5bb09d1b8..c06a7d388 100644
--- a/compiler-core/src/javascript/decision.rs
+++ b/compiler-core/src/javascript/decision.rs
@@ -600,8 +600,17 @@ impl<'generator, 'module, 'a> LetPrinter<'generator, 'module, 'a> {
         // need to perform. In particular if all size checks do not depend on any
         // previous segment then we can remove all the size checks except the last
         // one, as it implies all the other ones!
-        let mut most_restrictive_size_check = None;
-        for (variable, check) in &checks {
+
+        // First, we create a map from variable IDs to information about the
+        // size check we need to perform, as different bit array variables must
+        // still be checked separately.
+        let mut size_checks = HashMap::new();
+        // Since we are removing some checks, the check list will get shorter,
+        // shifting all the indices down. This variable keeps track of the current
+        // offset so we can correctly calculate the new index of each check.
+        let mut index_offset = 0;
+
+        for (index, (variable, check)) in checks.iter().enumerate() {
             if !check.referenced_segment_patterns().is_empty() {
                 // If any of the checks does reference a previous segment then
                 // it's not safe to remove intermediate checks! In that case we
@@ -614,11 +623,33 @@ impl<'generator, 'module, 'a> LetPrinter<'generator, 'module, 'a> {
                 test: BitArrayTest::Size(_),
             } = check
             {
-                most_restrictive_size_check = Some((variable.clone(), *check))
+                match size_checks.get_mut(&variable.id) {
+                    // If this is the first occurrence of a size check for this
+                    // variable, we record its position as well as the other
+                    // information.
+                    None => {
+                        _ = size_checks.insert(
+                            variable.id,
+                            SizeCheck {
+                                first_occurrence: index - index_offset,
+                                variable: variable.clone(),
+                                check,
+                            },
+                        )
+                    }
+                    // If another size check has already appeared, we simply replace
+                    // the check itself, as the later one is a more restrictive check.
+                    Some(size_check) => {
+                        size_check.check = *check;
+                        // We also increment the offset as the previous size check will
+                        // be removed, further offsetting any checks that come after this.
+                        index_offset += 1;
+                    }
+                }
             }
         }
 
-        let Some(size_check) = most_restrictive_size_check else {
+        if size_checks.is_empty() {
             // If there's no size test at all, then there's no meaningful optimisation
             // we can apply!
             return Some(ChecksAndBindings { checks, bindings });
@@ -635,7 +666,18 @@ impl<'generator, 'module, 'a> LetPrinter<'generator, 'module, 'a> {
             }
         });
 
-        checks.push_front(size_check);
+        let mut size_checks = size_checks.into_values().collect_vec();
+        // We must insert the size checks in the order they appear, to ensure the
+        // correct ordering of the final checks.
+        size_checks.sort_by_key(|check| check.first_occurrence);
+
+        for size_check in size_checks {
+            checks.insert(
+                size_check.first_occurrence,
+                (size_check.variable, size_check.check),
+            );
+        }
+
         Some(ChecksAndBindings { checks, bindings })
     }
 
@@ -664,7 +706,7 @@ impl<'generator, 'module, 'a> LetPrinter<'generator, 'module, 'a> {
                 // see if we can get to a failing node. We will only keep the
                 // results coming from the positive path!
                 for (check, decision) in choices {
-                    if let Some(mut checks) = self.positive_checks_and_bindings(decision) {
+                    if let Some(mut checks) = self.checks_and_bindings_loop(decision) {
                         checks.add_check(var.clone(), check);
                         result = Some(checks);
                     };
@@ -672,7 +714,7 @@ impl<'generator, 'module, 'a> LetPrinter<'generator, 'module, 'a> {
 
                 // Important not to forget the fallback check, that's a path we
                 // might have to go down to as well!
-                if let Some(mut checks) = self.positive_checks_and_bindings(fallback) {
+                if let Some(mut checks) = self.checks_and_bindings_loop(fallback) {
                     if let FallbackCheck::RuntimeCheck { check } = fallback_check {
                         checks.add_check(var.clone(), check);
                         result = Some(checks);
@@ -685,6 +727,13 @@ impl<'generator, 'module, 'a> LetPrinter<'generator, 'module, 'a> {
     }
 }
 
+#[derive(Debug)]
+struct SizeCheck<'a> {
+    first_occurrence: usize,
+    variable: Variable,
+    check: &'a RuntimeCheck,
+}
+
 /// The result we get from inspecting a `let`'s decision tree: it contains all
 /// the checks that lead down to the only possible successfull `Body` node and
 /// the bindings found inside it.


Test patch:
diff --git a/compiler-core/src/javascript/tests/bit_arrays.rs b/compiler-core/src/javascript/tests/bit_arrays.rs
index 4d81ee99e..fb953e745 100644
--- a/compiler-core/src/javascript/tests/bit_arrays.rs
+++ b/compiler-core/src/javascript/tests/bit_arrays.rs
@@ -2231,3 +2231,55 @@ pub fn go(x) {
 "#
     );
 }
+
+// https://github.com/gleam-lang/gleam/issues/4630
+#[test]
+fn tuple_bit_array() {
+    assert_js!(
+        "
+pub fn go(x) {
+  let assert #(<<>>) = x
+}
+"
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4630
+#[test]
+fn tuple_bit_array_case() {
+    assert_js!(
+        "
+pub fn go(x) {
+  case x {
+    #(<<>>) -> 1
+    _ -> 2
+  }
+}
+"
+    );
+}
+
+#[test]
+fn tuple_multiple_bit_arrays() {
+    assert_js!(
+        "
+pub fn go(x) {
+  let assert #(<<>>, <<1>>, <<2, 3>>) = x
+}
+"
+    );
+}
+
+#[test]
+fn tuple_multiple_bit_arrays_case() {
+    assert_js!(
+        "
+pub fn go(x) {
+  case x {
+    #(<<>>, <<1>>, <<2, 3>>) -> True
+    _ -> False
+  }
+}
+"
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap
index a5ab0001f..58fe661c0 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1818
 expression: "\nfn go(x) {\n  let size = 3\n  let assert <<1:size(size)-unit(2)>> = x\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -20,8 +18,8 @@ const FILEPATH = "src/module.gleam";
 function go(x) {
   let size = 3;
   if (
-    x.bitSize !== size * 2 ||
     size <= 0 ||
+    x.bitSize !== size * 2 ||
     bitArraySliceToInt(x, 0, size * 2, true, false) !== 1
   ) {
     throw makeError(
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size.snap
index a745337d1..f505ce496 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1051
 expression: "\nfn go(x) {\n  let n = 16\n  let assert <<a:bits-size(n)>> = x\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -19,7 +17,7 @@ const FILEPATH = "src/module.gleam";
 
 function go(x) {
   let n = 16;
-  if (x.bitSize !== n || n <= 0) {
+  if (n <= 0 || x.bitSize !== n) {
     throw makeError(
       "let_assert",
       FILEPATH,
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap
index 3191239f0..0dd43d2b2 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1078
 expression: "\nfn go(x) {\n  let n = 3\n  let assert <<a:bytes-size(n)>> = x\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -19,7 +17,7 @@ const FILEPATH = "src/module.gleam";
 
 function go(x) {
   let n = 3;
-  if (x.bitSize !== n * 8 || n <= 0) {
+  if (n <= 0 || x.bitSize !== n * 8) {
     throw makeError(
       "let_assert",
       FILEPATH,
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size.snap
index 89f12db97..679372698 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 939
 expression: "\nfn go(x) {\n  let n = 16\n  let assert <<a:size(n)>> = x\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -19,7 +17,7 @@ const FILEPATH = "src/module.gleam";
 
 function go(x) {
   let n = 16;
-  if (x.bitSize !== n || n <= 0) {
+  if (n <= 0 || x.bitSize !== n) {
     throw makeError(
       "let_assert",
       FILEPATH,
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_literal_value.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_literal_value.snap
index 3868a7062..babf3c2aa 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_literal_value.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_literal_value.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1024
 expression: "\nfn go(x) {\n  let n = 8\n  let assert <<a:size(n), 0b010101:size(8)>> = x\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -20,8 +18,8 @@ const FILEPATH = "src/module.gleam";
 function go(x) {
   let n = 8;
   if (
-    x.bitSize !== 8 + n ||
     n <= 0 ||
+    x.bitSize !== 8 + n ||
     bitArraySliceToInt(x, n, n + 8, true, false) !== 21
   ) {
     throw makeError(
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_shadowed_variable.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_shadowed_variable.snap
index 07d010a45..d6ecb89b2 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_shadowed_variable.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_size_shadowed_variable.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 995
 expression: "\nfn go(x) {\n  let n = 16\n  let n = 5\n  let assert <<a:size(n)>> = x\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -21,7 +19,7 @@ const FILEPATH = "src/module.gleam";
 function go(x) {
   let n = 16;
   let n$1 = 5;
-  if (x.bitSize !== n$1 || n$1 <= 0) {
+  if (n$1 <= 0 || x.bitSize !== n$1) {
     throw makeError(
       "let_assert",
       FILEPATH,
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_literal_unaligned_float.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_literal_unaligned_float.snap
index c3582702e..8621b8a56 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_literal_unaligned_float.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_literal_unaligned_float.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1360
 expression: "\nfn go(x) {\n  let n = 1\n  let assert <<_:size(n), 1.1, _:bits>> = x\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -20,8 +18,8 @@ const FILEPATH = "src/module.gleam";
 function go(x) {
   let n = 1;
   if (
-    x.bitSize < 64 + n ||
     n <= 0 ||
+    x.bitSize < 64 + n ||
     bitArraySliceToFloat(x, n, n + 64, true) !== 1.1
   ) {
     throw makeError(
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_bit_array.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_bit_array.snap
new file mode 100644
index 000000000..86ffba645
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_bit_array.snap
@@ -0,0 +1,30 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn go(x) {\n  let assert #(<<>>) = x\n}\n"
+---
+----- SOURCE CODE
+
+pub fn go(x) {
+  let assert #(<<>>) = x
+}
+
+
+----- COMPILED JAVASCRIPT
+import { makeError } from "../gleam.mjs";
+
+const FILEPATH = "src/module.gleam";
+
+export function go(x) {
+  if (x[0].bitSize !== 0) {
+    throw makeError(
+      "let_assert",
+      FILEPATH,
+      "my/mod",
+      3,
+      "go",
+      "Pattern match failed, no pattern matched the value.",
+      { value: x, start: 18, end: 40, pattern_start: 29, pattern_end: 36 }
+    )
+  }
+  return x;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_bit_array_case.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_bit_array_case.snap
new file mode 100644
index 000000000..df76a6a31
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_bit_array_case.snap
@@ -0,0 +1,23 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn go(x) {\n  case x {\n    #(<<>>) -> 1\n    _ -> 2\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn go(x) {
+  case x {
+    #(<<>>) -> 1
+    _ -> 2
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+export function go(x) {
+  let $ = x[0];
+  if ($.bitSize === 0) {
+    return 1;
+  } else {
+    return 2;
+  }
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_multiple_bit_arrays.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_multiple_bit_arrays.snap
new file mode 100644
index 000000000..4717cda87
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_multiple_bit_arrays.snap
@@ -0,0 +1,37 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn go(x) {\n  let assert #(<<>>, <<1>>, <<2, 3>>) = x\n}\n"
+---
+----- SOURCE CODE
+
+pub fn go(x) {
+  let assert #(<<>>, <<1>>, <<2, 3>>) = x
+}
+
+
+----- COMPILED JAVASCRIPT
+import { makeError } from "../gleam.mjs";
+
+const FILEPATH = "src/module.gleam";
+
+export function go(x) {
+  if (
+    x[2].bitSize !== 16 ||
+    x[2].byteAt(0) !== 2 ||
+    x[2].byteAt(1) !== 3 ||
+    x[1].bitSize !== 8 ||
+    x[1].byteAt(0) !== 1 ||
+    x[0].bitSize !== 0
+  ) {
+    throw makeError(
+      "let_assert",
+      FILEPATH,
+      "my/mod",
+      3,
+      "go",
+      "Pattern match failed, no pattern matched the value.",
+      { value: x, start: 18, end: 57, pattern_start: 29, pattern_end: 53 }
+    )
+  }
+  return x;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_multiple_bit_arrays_case.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_multiple_bit_arrays_case.snap
new file mode 100644
index 000000000..aa6286751
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__tuple_multiple_bit_arrays_case.snap
@@ -0,0 +1,49 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn go(x) {\n  case x {\n    #(<<>>, <<1>>, <<2, 3>>) -> True\n    _ -> False\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn go(x) {
+  case x {
+    #(<<>>, <<1>>, <<2, 3>>) -> True
+    _ -> False
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+export function go(x) {
+  let $ = x[2];
+  if ($.bitSize >= 8) {
+    if ($.byteAt(0) === 2) {
+      if ($.bitSize === 16) {
+        if ($.byteAt(1) === 3) {
+          let $1 = x[1];
+          if ($1.bitSize === 8) {
+            if ($1.byteAt(0) === 1) {
+              let $2 = x[0];
+              if ($2.bitSize === 0) {
+                return true;
+              } else {
+                return false;
+              }
+            } else {
+              return false;
+            }
+          } else {
+            return false;
+          }
+        } else {
+          return false;
+        }
+      } else {
+        return false;
+      }
+    } else {
+      return false;
+    }
+  } else {
+    return false;
+  }
+}