Repository: gleam-lang/gleam (2026-03-21T18:03:21Z)

Issue:
JS codegen regression 1.15.0: Internal Index based Reuse Error
<!--
Please include the following:
- Steps and code to reproduce the problem
- Versions for Gleam, OS, the runtime and any other software used
- Logs from running Gleam with the `GLEAM_LOG` environment variable set to `trace`
-->

## Basic Info

Gleam version: `1.15.0`
OS/Runtime: both locally on `macos Tahoe 26.1` and in my Github test runner `ubuntu-latest: 24.04`.
Reproduction: see below.

***


## Prelude

Ok so I did a simple `1.14.0` to `1.15.0` bump for [caffeine](https://github.com/Brickell-Research/caffeine_lang) and in doing so, the JS tests started to fail ([see here](https://github.com/Brickell-Research/caffeine_lang/actions/runs/23168227394/job/67313614593)) with:

```bash
Run cd caffeine_cli && gleam test --target javascript
  Resolving versions
  Compiling argv
  Compiling gleam_stdlib
  Compiling terra_madre
  Compiling caffeine_lang
  Compiling envoy
  Compiling filepath
  Compiling gleam_json
  Compiling gleam_community_colour
  Compiling gleam_regexp
  Compiling gleam_community_ansi
  Compiling gleeunit
  Compiling snag
  Compiling glint
  Compiling simplifile
  Compiling caffeine_cli
   Compiled in 0.51s
    Running caffeine_cli_test.main
file:///home/runner/work/caffeine_lang/caffeine_lang/caffeine_cli/build/dev/javascript/glint/glint.mjs:1932
  let $2 = $list.partition(
      ^

SyntaxError: Identifier '$2' has already been declared
    at compileSourceTextModule (node:internal/modules/esm/utils:346:16)
    at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:146:18)
    at #translate (node:internal/modules/esm/loader:497:12)
    at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:544:27)

Node.js v20.20.1
```

***

Peeking into the generated JS I did confirm:

```javascript
export function execute(glint, args) {
  let help_flag = flag_prefix + $help.help_flag.meta.name;
  let _block;
  let $1 = $list.partition(args, (s) => { return s === help_flag; });
  let $2 = $1[0];  <------------------------ INIT #1 --------------
  if ($2 instanceof $Empty) {
    let args$1 = $1[1];
    _block = [false, args$1];
  } else {
    let args$1 = $1[1];
    _block = [true, args$1];
  }
  let $ = _block;
  let help;
  let args$1;
  help = $[0];
  args$1 = $[1];
  let $2 = $list.partition( <------------------------ INIT #2 --------------
    args$1,
    (_capture) => { return $string.starts_with(_capture, flag_prefix); },
  );
  let flags;
  let args$2;
  flags = $2[0];
  args$2 = $2[1];
  return do_execute(glint.cmd, glint.config, args$2, flags, help, toList([]));
}
```

This is the esmodule for [glint](https://github.com/TanklesXL/glint).

***

## Minimal Reproduction of Bug

So I did the thing we do in `'26` and I asked Claude to produce a minimal reproduction.

It landed on:

```gleam
pub fn main() {
  let x =
    case #(1, 2) {
      #(1, b) -> b
      #(_, b) -> b
    }

  let #(a, _b) = #(x, 3)

  a
}
```

Which yields:

```javascript
export function main() {
  let _block;
  let $ = [1, 2];
  let $1 = $[0]; <------------------------ INIT #1 --------------
  if ($1 === 1) {
    let b = $[1];
    _block = b;
  } else {
    let b = $[1];
    _block = b;
  }
  let x = _block;
  let $1 = [x, 3]; <------------------------ INIT #2 --------------
  let a;
  a = $1[0];
  return a;
}
```

So had a simple test:

```gleam
import gleeunit
import one_fifteen_js_bug

pub fn main() -> Nil {
  gleeunit.main()
}

pub fn repro_test() {
  one_fifteen_js_bug.main()
}

```

Which when we run tests `gleam test --target=javascript` returns (_abridged_):

```bash
   Compiled in 0.01s
    Running one_fifteen_js_bug_test.main
file:///Users/rdurst/BrickellResearch/one_fifteen_js_bug/build/dev/javascript/one_fifteen_js_bug/one_fifteen_js_bug.mjs:13
  let $1 = [x, 3];
      ^

SyntaxError: Identifier '$1' has already been declared
    at compileSourceTextModule (node:internal/modules/esm/utils:346:16)
    at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:107:18)
    at #translate (node:internal/modules/esm/loader:536:12)
    at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:583:27)
    at async ModuleJob._link (node:internal/modules/esm/module_job:162:19)

Node.js v22.18.0
```



Discussion:
Apologies for not doing a quick `brew upgrade gleam` and `gleam test && gleam test --target=javascript` type thing for the last release clients!! Would have been an easy catch then.
Claude also pointed to https://github.com/gleam-lang/gleam/pull/5271. So I compiled Gleam locally, doing a before after of that and it does seem to be the culprit.

`29a3335a438b95c1632dce79fa7b994105e5912f` - passes above ✅
`ac7ee857d0e05a96199aa4a9258c7f1f17536759` - failes above ❌ 
 
Process:
1. cd to `gleam` codebase
2. `git checkout 29a3335a438b95c1632dce79fa7b994105e5912f`
3. `cargo build --release`
4. cd to minimal repo
5. remove build cache
6. `DIR_TO_GLEAM/gleam/target/release/gleam test --target=javascript` --> fails
7. cd back to `gleam` codebase
8. `git checkout ac7ee857d0e05a96199aa4a9258c7f1f17536759`
9. `cargo build --release`
10. cd to minimal repo again
11. remove build cache
12. `DIR_TO_GLEAM/gleam/target/release/gleam test --target=javascript` --> succeeds
Thank you
Seeing PRs tagging this and getting excited!! Thanks folks! 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ac9537098e..78de063eae2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -215,6 +215,11 @@
 - Fixed the formatting of some errors' hints to properly wrap.
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+- Fixed a bug where the JavaScript code generator could produce duplicate `let`
+  declarations for internal variables after a `case` expression whose subject
+  directly matches one of the branches.
+  ([Eyup Can Akman](https://github.com/eyupcanakman))
+
 ## v1.15.1 - 2026-03-17
 
 ### Bug fixes
diff --git a/compiler-core/src/javascript/decision.rs b/compiler-core/src/javascript/decision.rs
index 10095175422..f398a911c62 100644
--- a/compiler-core/src/javascript/decision.rs
+++ b/compiler-core/src/javascript/decision.rs
@@ -391,7 +391,51 @@ impl<'a> CasePrinter<'_, '_, 'a, '_> {
             if let FallbackCheck::RuntimeCheck { check } = fallback_check {
                 self.variables.record_check_assignments(var, check);
             }
-            return self.inside_new_scope(|this| this.decision(fallback));
+
+            // We can't use `inside_new_scope` here without care: the
+            // code we generate goes directly into the enclosing scope
+            // (there's no wrapping if-else block), so the `$` variable
+            // counter must not be reset or later code could redeclare
+            // one of the `$N` variables introduced here.
+            let old_scope = match &self.kind {
+                DecisionKind::Case { .. } => self
+                    .variables
+                    .expression_generator
+                    .current_scope_vars
+                    .clone(),
+                DecisionKind::LetAssert { .. } => Default::default(),
+            };
+            let old_names = self.variables.scoped_variable_names.clone();
+            let old_segments = self.variables.segment_values.clone();
+            let old_segment_names = self.variables.scoped_segment_names.clone();
+
+            let result = self.decision(fallback);
+
+            match &self.kind {
+                DecisionKind::Case { .. } => {
+                    let assignment_var: EcoString = ASSIGNMENT_VAR.into();
+                    let counter = self
+                        .variables
+                        .expression_generator
+                        .current_scope_vars
+                        .get(&assignment_var)
+                        .copied();
+                    self.variables.expression_generator.current_scope_vars = old_scope;
+                    if let Some(n) = counter {
+                        let _ = self
+                            .variables
+                            .expression_generator
+                            .current_scope_vars
+                            .insert(assignment_var, n);
+                    }
+                }
+                DecisionKind::LetAssert { .. } => {}
+            }
+
+            self.variables.scoped_variable_names = old_names;
+            self.variables.segment_values = old_segments;
+            self.variables.scoped_segment_names = old_segment_names;
+            return result;
         }
 
         // Otherwise we'll have to generate a series of if-else to check which


Test patch:
diff --git a/compiler-core/src/javascript/tests/case.rs b/compiler-core/src/javascript/tests/case.rs
index fa68c1f0b53..8c7cc86af81 100644
--- a/compiler-core/src/javascript/tests/case.rs
+++ b/compiler-core/src/javascript/tests/case.rs
@@ -968,3 +968,22 @@ pub fn parse_digit(s: String) -> Int {
 "#
     );
 }
+
+// https://github.com/gleam-lang/gleam/issues/5467
+#[test]
+fn no_duplicate_let_after_directly_matching_case() {
+    assert_js!(
+        r#"
+pub fn go() {
+  let x =
+    case #(1, 2) {
+      #(1, b) -> b
+      #(_, b) -> b
+    }
+
+  let #(a, _b) = #(x, 3)
+
+  a
+}"#
+    )
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_renaming.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_renaming.snap
index 1add24b5e3e..fd83832e100 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_renaming.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_renaming.snap
@@ -1,6 +1,8 @@
 ---
 source: compiler-core/src/javascript/tests/assignments.rs
+assertion_line: 37
 expression: "\n\npub fn go(x, wibble) {\n  let a = 1\n  wibble(a)\n  let a = 2\n  wibble(a)\n  let assert #(a, 3) = x\n  let b = a\n  wibble(b)\n  let c = {\n    let a = a\n    #(a, b)\n  }\n  wibble(a)\n  // make sure arguments are counted in initial state\n  let x = c\n  x\n}\n"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_used_in_pattern_and_assignment.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_used_in_pattern_and_assignment.snap
index 8ca89cf41ce..7a34d5a4c1b 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_used_in_pattern_and_assignment.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__variable_used_in_pattern_and_assignment.snap
@@ -1,6 +1,8 @@
 ---
 source: compiler-core/src/javascript/tests/assignments.rs
+assertion_line: 129
 expression: "pub fn main(x) {\n  let #(x) = #(x)\n  x\n}\n"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 pub fn main(x) {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_building_list_matched_by_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_building_list_matched_by_pattern.snap
index 11ab1915326..3626c2c3c34 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_building_list_matched_by_pattern.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_building_list_matched_by_pattern.snap
@@ -1,6 +1,8 @@
 ---
 source: compiler-core/src/javascript/tests/case.rs
+assertion_line: 461
 expression: "pub fn go(x) {\n   case x {\n     [] -> []\n     [a, b] -> [a, b]\n     [1, ..rest] -> [1, ..rest]\n     _ -> x\n   }\n}"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 pub fn go(x) {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_multiple_subjects_building_list_matched_by_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_multiple_subjects_building_list_matched_by_pattern.snap
index 97dce629e41..9c6e392fbda 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_multiple_subjects_building_list_matched_by_pattern.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_multiple_subjects_building_list_matched_by_pattern.snap
@@ -1,6 +1,8 @@
 ---
 source: compiler-core/src/javascript/tests/case.rs
+assertion_line: 804
 expression: "pub fn go(n, x) {\n   case n, x {\n     1, [] -> []\n     _, [a, b] -> [a, b]\n     3, [1, ..rest] -> [1, ..rest]\n     _, _ -> x\n   }\n}"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 pub fn go(n, x) {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__no_duplicate_let_after_directly_matching_case.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__no_duplicate_let_after_directly_matching_case.snap
new file mode 100644
index 00000000000..157939f36ff
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__no_duplicate_let_after_directly_matching_case.snap
@@ -0,0 +1,38 @@
+---
+source: compiler-core/src/javascript/tests/case.rs
+assertion_line: 950
+expression: "\npub fn go() {\n  let x =\n    case #(1, 2) {\n      #(1, b) -> b\n      #(_, b) -> b\n    }\n\n  let #(a, _b) = #(x, 3)\n\n  a\n}"
+snapshot_kind: text
+---
+----- SOURCE CODE
+
+pub fn go() {
+  let x =
+    case #(1, 2) {
+      #(1, b) -> b
+      #(_, b) -> b
+    }
+
+  let #(a, _b) = #(x, 3)
+
+  a
+}
+
+----- COMPILED JAVASCRIPT
+export function go() {
+  let _block;
+  let $ = [1, 2];
+  let $1 = $[0];
+  if ($1 === 1) {
+    let b = $[1];
+    _block = b;
+  } else {
+    let b = $[1];
+    _block = b;
+  }
+  let x = _block;
+  let $2 = [x, 3];
+  let a;
+  a = $2[0];
+  return a;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__alternative_patterns_list.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__alternative_patterns_list.snap
index d71e0638235..395e2dafee6 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__alternative_patterns_list.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__alternative_patterns_list.snap
@@ -1,6 +1,8 @@
 ---
 source: compiler-core/src/javascript/tests/case_clause_guards.rs
+assertion_line: 187
 expression: "pub fn main(xs) -> Int {\n  case xs {\n    [1] | [1, 2] -> 0\n    _ -> 1\n  }\n}\n"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 pub fn main(xs) -> Int {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_mixed_fields_first_unlabelled.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_mixed_fields_first_unlabelled.snap
index 371e3a19fb5..9847164a623 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_mixed_fields_first_unlabelled.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_mixed_fields_first_unlabelled.snap
@@ -1,6 +1,8 @@
 ---
 source: compiler-core/src/javascript/tests/custom_types.rs
+assertion_line: 281
 expression: "\npub type Cat {\n  Cat(String, cuteness: Int)\n}\n\npub fn go(cat) {\n  let Cat(x, y) = cat\n  let Cat(cuteness: y, ..) = cat\n  let Cat(x, cuteness: y) = cat\n  x\n}\n\n"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_named_fields.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_named_fields.snap
index 8349087d9f9..38feb627096 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_named_fields.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_named_fields.snap
@@ -1,6 +1,8 @@
 ---
 source: compiler-core/src/javascript/tests/custom_types.rs
+assertion_line: 262
 expression: "\npub type Cat {\n  Cat(name: String, cuteness: Int)\n}\n\npub fn go(cat) {\n  let Cat(x, y) = cat\n  let Cat(name: x, ..) = cat\n  let assert Cat(cuteness: 4, name: x) = cat\n  x\n}\n\n"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 
diff --git a/test/language/test/language/directly_matching_case_subject_test.gleam b/test/language/test/language/directly_matching_case_subject_test.gleam
index d636648ae09..bc144e5f8e3 100644
--- a/test/language/test/language/directly_matching_case_subject_test.gleam
+++ b/test/language/test/language/directly_matching_case_subject_test.gleam
@@ -17,3 +17,14 @@ pub fn directly_matching_case_subject_test() {
 
   assert result == "ABC"
 }
+
+// github.com/gleam-lang/gleam/issues/5467
+pub fn no_duplicate_let_after_directly_matching_case_test() {
+  let x =
+    case #(1, 2) {
+      #(1, b) -> b
+      #(_, b) -> b
+    }
+  let #(a, _b) = #(x, 3)
+  assert a == 2
+}