Repository: gleam-lang/gleam (Rust)

Issue:
javascript codegen bug with nested case patterns
The following code prints `undefined` on `gleam 1.6.2`

```gleam
import gleam/io

pub type Wibble {
  Wibble(wobble: String)
}

pub fn main() {
  let tmp = Wibble(wobble: "wibble")
  case tmp {
    Wibble(wobble: "w" as wibble <> _wobble) -> io.println(wibble)
    _ -> panic
  }
}
```

It generates the following JS, with `let wibble = "w".wobble;` being the culprit.

```js
import * as $io from "../gleam_stdlib/gleam/io.mjs";
import { CustomType as $CustomType, makeError } from "./gleam.mjs";

export class Wibble extends $CustomType {
  constructor(wobble) {
    super();
    this.wobble = wobble;
  }
}

export function main() {
  let tmp = new Wibble("wibble");
  if (tmp instanceof Wibble && tmp.wobble.startsWith("w")) {
    let wibble = "w".wobble; // <<<<<< THIS IS THE ISSUE >>>>>>>
    return $io.println(wibble);
  } else {
    throw makeError(
      "panic",
      "jsgen",
      11,
      "main",
      "`panic` expression evaluated.",
      {}
    )
  }
}
```

Thanks to @joshi-monster for spotting this one!


Discussion:
Thank you
Working on this^^ :purple_heart: 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d9cb6f0960..1386752e048 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -175,6 +175,10 @@
   pipelines that would be safe to remove.
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+ - Fixed a bug where assigning the prefix of a string pattern to a variable
+   nested inside another pattern would produce invalid code on Javascript.
+   ([yoshi](https://github.com/joshi-monster))
+
 ## v1.6.1 - 2024-11-19
 
 ### Bug fixed
diff --git a/compiler-core/src/javascript/pattern.rs b/compiler-core/src/javascript/pattern.rs
index d1610dd8201..7b19ca2cfff 100644
--- a/compiler-core/src/javascript/pattern.rs
+++ b/compiler-core/src/javascript/pattern.rs
@@ -519,7 +519,16 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, '
                     // let prefix = "wibble";
                     // ^^^^^^^^^^^^^^^^^^^^^ we're adding this assignment inside the if clause
                     //                       the case branch gets translated into.
-                    self.push_assignment(expression::string(left_side_string), left);
+                    //
+                    // We also want to push this assignment without using push_assignment, since we
+                    // do _not_ want to access the current path on the static string!
+                    let var = self.next_local_var(left);
+                    self.assignments.push(Assignment {
+                        subject: expression::string(left_side_string),
+                        path: docvec![],
+                        name: left,
+                        var,
+                    });
                 }
                 Ok(())
             }


Test patch:
diff --git a/compiler-core/src/javascript/tests/assignments.rs b/compiler-core/src/javascript/tests/assignments.rs
index dc4844df33d..6a5fd4109ef 100644
--- a/compiler-core/src/javascript/tests/assignments.rs
+++ b/compiler-core/src/javascript/tests/assignments.rs
@@ -185,6 +185,23 @@ pub fn main() {
     );
 }
 
+// https://github.com/gleam-lang/gleam/issues/3894
+#[test]
+fn let_assert_nested_string_prefix() {
+    assert_js!(
+        r#"
+type Wibble {
+  Wibble(wibble: String)
+}
+
+pub fn main() {
+  let assert Wibble(wibble: "w" as prefix <> rest) = Wibble("wibble")
+  prefix <> rest
+}
+"#
+    );
+}
+
 // https://github.com/gleam-lang/gleam/issues/2931
 #[test]
 fn keyword_assignment() {
diff --git a/compiler-core/src/javascript/tests/case.rs b/compiler-core/src/javascript/tests/case.rs
index 03735abe5e0..a3fc9179000 100644
--- a/compiler-core/src/javascript/tests/case.rs
+++ b/compiler-core/src/javascript/tests/case.rs
@@ -267,3 +267,48 @@ pub fn main() {
 "#
     )
 }
+
+// https://github.com/gleam-lang/gleam/issues/3894
+#[test]
+fn nested_string_prefix_assignment() {
+    assert_js!(
+        r#"
+type Wibble {
+  Wibble(wobble: String)
+}
+
+pub fn main() {
+  let tmp = Wibble(wobble: "wibble")
+  case tmp {
+    Wibble(wobble: "w" as wibble <> rest) -> wibble <> rest
+    _ -> panic
+  }
+}
+"#
+    )
+}
+
+#[test]
+fn deeply_nested_string_prefix_assignment() {
+    assert_js!(
+        r#"
+type Wibble {
+  Wibble(Wobble)
+}            
+type Wobble {
+  Wobble(wabble: Wabble)
+}
+type Wabble {
+  Wabble(tuple: #(Int, String))
+}
+
+pub fn main() {
+  let tmp = Wibble(Wobble(Wabble(#(42, "wibble"))))
+  case tmp {
+    Wibble(Wobble(Wabble(#(_int, "w" as wibble <> rest)))) -> wibble <> rest
+    _ -> panic
+  }
+}
+"#
+    )
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__let_assert_nested_string_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__let_assert_nested_string_prefix.snap
new file mode 100644
index 00000000000..a9fcc51791c
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__let_assert_nested_string_prefix.snap
@@ -0,0 +1,42 @@
+---
+source: compiler-core/src/javascript/tests/assignments.rs
+expression: "\ntype Wibble {\n  Wibble(wibble: String)\n}\n\npub fn main() {\n  let assert Wibble(wibble: \"w\" as prefix <> rest) = Wibble(\"wibble\")\n  prefix <> rest\n}\n"
+---
+----- SOURCE CODE
+
+type Wibble {
+  Wibble(wibble: String)
+}
+
+pub fn main() {
+  let assert Wibble(wibble: "w" as prefix <> rest) = Wibble("wibble")
+  prefix <> rest
+}
+
+
+----- COMPILED JAVASCRIPT
+import { CustomType as $CustomType, makeError } from "../gleam.mjs";
+
+class Wibble extends $CustomType {
+  constructor(wibble) {
+    super();
+    this.wibble = wibble;
+  }
+}
+
+export function main() {
+  let $ = new Wibble("wibble");
+  if (!($ instanceof Wibble) || !$.wibble.startsWith("w")) {
+    throw makeError(
+      "let_assert",
+      "my/mod",
+      7,
+      "main",
+      "Pattern match failed, no pattern matched the value.",
+      { value: $ }
+    )
+  }
+  let rest = $.wibble.slice(1);
+  let prefix = "w";
+  return prefix + rest;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__deeply_nested_string_prefix_assignment.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__deeply_nested_string_prefix_assignment.snap
new file mode 100644
index 00000000000..bd745ab93b9
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__deeply_nested_string_prefix_assignment.snap
@@ -0,0 +1,69 @@
+---
+source: compiler-core/src/javascript/tests/case.rs
+expression: "\ntype Wibble {\n  Wibble(Wobble)\n}            \ntype Wobble {\n  Wobble(wabble: Wabble)\n}\ntype Wabble {\n  Wabble(tuple: #(Int, String))\n}\n\npub fn main() {\n  let tmp = Wibble(Wobble(Wabble(#(42, \"wibble\"))))\n  case tmp {\n    Wibble(Wobble(Wabble(#(_int, \"w\" as wibble <> rest)))) -> wibble <> rest\n    _ -> panic\n  }\n}\n"
+---
+----- SOURCE CODE
+
+type Wibble {
+  Wibble(Wobble)
+}            
+type Wobble {
+  Wobble(wabble: Wabble)
+}
+type Wabble {
+  Wabble(tuple: #(Int, String))
+}
+
+pub fn main() {
+  let tmp = Wibble(Wobble(Wabble(#(42, "wibble"))))
+  case tmp {
+    Wibble(Wobble(Wabble(#(_int, "w" as wibble <> rest)))) -> wibble <> rest
+    _ -> panic
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+import { CustomType as $CustomType, makeError } from "../gleam.mjs";
+
+class Wibble extends $CustomType {
+  constructor(x0) {
+    super();
+    this[0] = x0;
+  }
+}
+
+class Wobble extends $CustomType {
+  constructor(wabble) {
+    super();
+    this.wabble = wabble;
+  }
+}
+
+class Wabble extends $CustomType {
+  constructor(tuple) {
+    super();
+    this.tuple = tuple;
+  }
+}
+
+export function main() {
+  let tmp = new Wibble(new Wobble(new Wabble([42, "wibble"])));
+  if (tmp instanceof Wibble &&
+  tmp[0] instanceof Wobble &&
+  tmp[0].wabble instanceof Wabble &&
+  tmp[0].wabble.tuple[1].startsWith("w")) {
+    let rest = tmp[0].wabble.tuple[1].slice(1);
+    let wibble = "w";
+    return wibble + rest;
+  } else {
+    throw makeError(
+      "panic",
+      "my/mod",
+      16,
+      "main",
+      "`panic` expression evaluated.",
+      {}
+    )
+  }
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__nested_string_prefix_assignment.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__nested_string_prefix_assignment.snap
new file mode 100644
index 00000000000..70e0a5a5db3
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__nested_string_prefix_assignment.snap
@@ -0,0 +1,46 @@
+---
+source: compiler-core/src/javascript/tests/case.rs
+expression: "\ntype Wibble {\n  Wibble(wobble: String)\n}\n\npub fn main() {\n  let tmp = Wibble(wobble: \"wibble\")\n  case tmp {\n    Wibble(wobble: \"w\" as wibble <> rest) -> wibble <> rest\n    _ -> panic\n  }\n}\n"
+---
+----- SOURCE CODE
+
+type Wibble {
+  Wibble(wobble: String)
+}
+
+pub fn main() {
+  let tmp = Wibble(wobble: "wibble")
+  case tmp {
+    Wibble(wobble: "w" as wibble <> rest) -> wibble <> rest
+    _ -> panic
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+import { CustomType as $CustomType, makeError } from "../gleam.mjs";
+
+class Wibble extends $CustomType {
+  constructor(wobble) {
+    super();
+    this.wobble = wobble;
+  }
+}
+
+export function main() {
+  let tmp = new Wibble("wibble");
+  if (tmp instanceof Wibble && tmp.wobble.startsWith("w")) {
+    let rest = tmp.wobble.slice(1);
+    let wibble = "w";
+    return wibble + rest;
+  } else {
+    throw makeError(
+      "panic",
+      "my/mod",
+      10,
+      "main",
+      "`panic` expression evaluated.",
+      {}
+    )
+  }
+}