Repository: gleam-lang/gleam (rust)

Issue:
Syntax error on JavaScript for specific number syntax
In Gleam, numbers are allowed to look something like this: `0x_abcd`, whereas in JavaScript, the `0x` prefix (along with `0b` and `0o`) cannot be followed immediately by an underscore.
Since the Gleam compiler currently performs no transformation on numbers when compiling to JavaScript, this causes a syntax error.
This is not a problem on Erlang as the compiler already has to transform `0x` in `16#`, and the invalid syntax is eliminated in the process.
We can probably just check for `0b_`, `0x_` and `0o_` at the start of numbers and remove the underscore in that case.

Pull request description:
Fix bug with js numbers
Fixes #4459

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 74435f2ba..28d629df3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@
   unqualified import even if it was already imported.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- Fixed a bug where numbers starting with `0x_`, `0o_` and `0b_` would cause
+  a syntax error when compiling to JavaScript.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ## v1.10.0-rc1 - 2025-04-05
 
 ### Compiler
diff --git a/Cargo.lock b/Cargo.lock
index 7824977b8..856ec5f45 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3122,9 +3122,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.39.2"
+version = "1.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
+checksum = "2209a14885b74764cce87ffa777ffa1b8ce81a3f3166c6f886b83337fe7e077f"
 dependencies = [
  "backtrace",
  "bytes",
diff --git a/compiler-core/src/javascript/expression.rs b/compiler-core/src/javascript/expression.rs
index 8173b49dc..ffce02fb7 100644
--- a/compiler-core/src/javascript/expression.rs
+++ b/compiler-core/src/javascript/expression.rs
@@ -1461,6 +1461,10 @@ 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');


Test patch:
diff --git a/compiler-core/src/javascript/tests/numbers.rs b/compiler-core/src/javascript/tests/numbers.rs
index 36a8547dd..5176cfaf2 100644
--- a/compiler-core/src/javascript/tests/numbers.rs
+++ b/compiler-core/src/javascript/tests/numbers.rs
@@ -313,3 +313,39 @@ fn main(x) {
 "#
     );
 }
+
+// https://github.com/gleam-lang/gleam/issues/4459
+#[test]
+fn underscore_after_hexadecimal_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0x_12_34
+}
+"
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4459
+#[test]
+fn underscore_after_octal_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0o_12_34
+}
+"
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4459
+#[test]
+fn underscore_after_binary_prefix() {
+    assert_js!(
+        "
+pub fn main() {
+  0b_10_01
+}
+"
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_binary_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_binary_prefix.snap
new file mode 100644
index 000000000..f8b7e790e
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_binary_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0b_10_01\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0b_10_01
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0b10_01;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_hexadecimal_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_hexadecimal_prefix.snap
new file mode 100644
index 000000000..60b647303
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_hexadecimal_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0x_12_34\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0x_12_34
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0x12_34;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_octal_prefix.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_octal_prefix.snap
new file mode 100644
index 000000000..f72b3c280
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__numbers__underscore_after_octal_prefix.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/javascript/tests/numbers.rs
+expression: "\npub fn main() {\n  0o_12_34\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  0o_12_34
+}
+
+
+----- COMPILED JAVASCRIPT
+export function main() {
+  return 0o12_34;
+}