Repository: gleam-lang/gleam (rust)

Issue:
JS codegen fails to use proper division and remainder functions in case guards
Demonstration
=============

Wrong behaviour when dividing numbers in case guards on the JS target:

```gleam
  case 9 / 2 {
    result if result == 9 / 2 -> "
      We expect this branch to run,
      because of course 9 / 2 == 9 / 2
      except...
      "
    _ -> "
      On JS we end up asking whether 4 == 4.5
      because integer division is truncated in gleam expressions
      and because case guards don't use proper gleam integer division.
      So the wrong case branch is run.
      "
  }
  |> io.println
  
  case 17.0 /. 0.0 {
    result if result == 17.0 /. 0.0 -> "
      We expect this branch to run,
      because of course 17.0 /. 0.0 == 17.0 /. 0.0
      except...
      "
    _ -> "
      On JS we end up asking whether 0 == Infinity
      because float division-by-0 gives 0 in gleam expressions
      and because case guards don't use proper gleam float division.
      So the wrong case branch is run.
      "
  }
  |> io.println
  
  case 403 % 0 {
    result if result == 403 % 0 -> "
      We expect this branch to run,
      because of course 403 % 0 == 403 % 0
      except...
      "
    _ -> "
      On JS we end up asking whether 0 == NaN
      because remainder-by-0 gives 0 in gleam expressions
      and because case guards don't use proper gleam remaindering.
      So the wrong case branch is run.
      "
  }
  |> io.println
```

[Working demo in the gleam playground _(version 1.8.0)_.](https://playground.gleam.run/#N4IgbgpgTgzglgewHYgFwEYA0IDGyAuES+aIcAtgA4JT4AEA5gDYQCG5A9IgDpK+UBXAEZ0AZkjrlWcJAAoAlHWC9edOhw50AYjToApAMp08AEwgMimOgAMO1ujAAWCAUxN0hEPOQg2TcMDgzAElia1V1TVYkd1sAOnsnFzcPLwQfPwCgiC0mBFZ8azjiiI06aNiAUkTnV3dPb19rKAgpGTMoUMLiuNLNCAAPHAhKenxHCABPOhNkAHJ6GWNWGF8GAVYoExgAQgi+ugBhCZwAazpx324QQ/TKOBZ3PVYwFZwoOFHri9YRfAQHBBfOM4DA6IA+DcAg/vgwBD++DAMP7+wky1WdAAnOo6AAmJQRNQtGCuRaiOgEol0AC8FPRmJxAFoAHx0a54tR0ADqvkGlC8Y0coI8UGiOEcFwBUAESEwrLUDVYAlRCBJeAEsF8GM0OKpNM1Mrog2Gox6epZyLUAH06IzmSA9QB5CSGOgAdy5MToAko5RgpxkDBdE0uUDoABZKdSQ3EAKx6uUK3wyQgWYP+QLwZB0AX4CVIHAFCDuJbMNjkfUDSgE9NIGB6iqpPPxlFrDZbMGzJALD2oisIHnB4vsTPEczQGZZKu9M1sgwAy4uqDIf151FCIW50UCnOTtls01qAC+EQAPkzEHEK4mmHxkRFl750AB2OIABnUcToz5fuKnZKYxNJECEn+4Z0I+X4cO+n6vtae47hyXLlryFz8mCq7CqK/ykpK0pTrKXjyoqyouGqoFPq+EEfl+2pgeRkEvnqBojPgxq4TarKWjBtqsQ6+hGK6+rup63q+kg/rOoGEzBq+2qhKIMhwPgkyxvhjaiHkBRjmmiBIHSQiTHSr4MAEgEfkOjAsIO3KVtpNasXWcaonejAttsMzzPQjY9n25klmI6n0KmoLaducEzshvjOguolNoK6GZmCW4mlxB7HqeCDnh8xBXkiahOSGz4AMx0JUpnKD+gHknAJK/vQ2r5UVJXQUysE7pyZY8jgfICmh65ilhUrKQ2hHGMRqL1cVpl1YVE3PgxQxMSxcFsVOHHNclcE8U6/FEO4QkrCJYkSaO0nUgAcqwp2DQRvgtG0MTQLp+mGcZYKvkWFmllZgFVrZS32SpjkrM2myue2naeQu3kDqWt3SPdmUMCFO5hXOkWLjFPUivF/VI7u62HsiJ6ZulF5Zdeai8ATID7kAA)

Culprit
=======

The gleam playground has a particularly helpful tab that shows the JS output. The issue seems to be that JS, with its single `Number` type, has to be asked nicely to do division how gleam does (truncating integer division properly, and returning 0 upon division-by-0 for both ints and floats). The gleam compiler usually does this using the functions `divideInt` and `divideFloat` in the JS output instead of the usual JS `/` operator, and `remainderInt` instead of the `%` operator. For correct integer division, remaindering, and float division see [expression.rs, lines 944-963](https://github.com/gleam-lang/gleam/blob/aab05d489a134a2db12ae8608bd22ba74dadbff4/compiler-core/src/javascript/expression.rs#L944-L963).

Compare the correct implementations linked above with the code implementing division and remaindering in case guards : [pattern.rs, lines 334-344](https://github.com/gleam-lang/gleam/blob/aab05d489a134a2db12ae8608bd22ba74dadbff4/compiler-core/src/javascript/pattern.rs#L334-L344). Here both forms of division just use the JS `/` operator, and remaindering just uses `%`. **I believe the fix involves separating out the two kinds of division and replacing `/` or `%` with the correct functions (and tracking the usage of those functions appropriately - I have no idea how difficult this part will be).**

Apology
=======

This was a fun bug to find. I would really love to have a go at fixing bugs like these when I find them but I can never seem to get rust working on my machine well enough to build gleam. I tried again today, but still had no luck... so I just wrote up this issue as nicely as I could (sorry).

Pull request description:
Fix proper codegen of guard arithmetic on js target
This PR fixes #4241 
If you decide this is a breaking change, feel free to close this PR

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c0086066..3f0022119 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -98,6 +98,10 @@
 
 ### Bug fixes
 
+- Fixed a bug where division and remainder operators would not work correctly
+  in guards on the JavaScript target.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ## v1.8.1 - 2025-02-11
 
 ### Bug fixes
diff --git a/compiler-core/src/javascript/pattern.rs b/compiler-core/src/javascript/pattern.rs
index 56b40c429..4257fe7cd 100644
--- a/compiler-core/src/javascript/pattern.rs
+++ b/compiler-core/src/javascript/pattern.rs
@@ -331,16 +331,25 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, '
                 docvec![left, " * ", right]
             }
 
-            ClauseGuard::DivFloat { left, right, .. } | ClauseGuard::DivInt { left, right, .. } => {
+            ClauseGuard::DivFloat { left, right, .. } => {
                 let left = self.wrapped_guard(left)?;
                 let right = self.wrapped_guard(right)?;
-                docvec![left, " / ", right]
+                self.expression_generator.tracker.float_division_used = true;
+                docvec!["divideFloat", wrap_args([left, right])]
+            }
+
+            ClauseGuard::DivInt { left, right, .. } => {
+                let left = self.wrapped_guard(left)?;
+                let right = self.wrapped_guard(right)?;
+                self.expression_generator.tracker.int_division_used = true;
+                docvec!["divideInt", wrap_args([left, right])]
             }
 
             ClauseGuard::RemainderInt { left, right, .. } => {
                 let left = self.wrapped_guard(left)?;
                 let right = self.wrapped_guard(right)?;
-                docvec![left, " % ", right]
+                self.expression_generator.tracker.int_remainder_used = true;
+                docvec!["remainderInt", wrap_args([left, right])]
             }
 
             ClauseGuard::Or { left, right, .. } => {


Test patch:
diff --git a/compiler-core/src/javascript/tests/case_clause_guards.rs b/compiler-core/src/javascript/tests/case_clause_guards.rs
index cf24d633a..eb6dd081d 100644
--- a/compiler-core/src/javascript/tests/case_clause_guards.rs
+++ b/compiler-core/src/javascript/tests/case_clause_guards.rs
@@ -531,3 +531,48 @@ fn constructor_function_in_guard() {
     "#,
     );
 }
+
+// https://github.com/gleam-lang/gleam/issues/4241
+#[test]
+fn int_division() {
+    assert_js!(
+        r#"
+pub fn main() {
+  case 5 / 2 {
+    x if x == 5 / 2 -> True
+    _ -> False
+  }
+}
+"#,
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4241
+#[test]
+fn float_division() {
+    assert_js!(
+        r#"
+pub fn main() {
+  case 5.1 /. 0.0 {
+    x if x == 5.1 /. 0.0 -> True
+    _ -> False
+  }
+}
+"#,
+    );
+}
+
+// https://github.com/gleam-lang/gleam/issues/4241
+#[test]
+fn int_remainder() {
+    assert_js!(
+        r#"
+pub fn main() {
+  case 4 % 0 {
+    x if x == 4 % 0 -> True
+    _ -> False
+  }
+}
+"#,
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__float_division.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__float_division.snap
new file mode 100644
index 000000000..9286ae5d2
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__float_division.snap
@@ -0,0 +1,26 @@
+---
+source: compiler-core/src/javascript/tests/case_clause_guards.rs
+expression: "\npub fn main() {\n  case 5.1 /. 0.0 {\n    x if x == 5.1 /. 0.0 -> True\n    _ -> False\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  case 5.1 /. 0.0 {
+    x if x == 5.1 /. 0.0 -> True
+    _ -> False
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+import { divideFloat } from "../gleam.mjs";
+
+export function main() {
+  let $ = divideFloat(5.1, 0.0);
+  if ($ === (divideFloat(5.1, 0.0))) {
+    let x = $;
+    return true;
+  } else {
+    return false;
+  }
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__int_division.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__int_division.snap
new file mode 100644
index 000000000..62c82a5ae
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__int_division.snap
@@ -0,0 +1,26 @@
+---
+source: compiler-core/src/javascript/tests/case_clause_guards.rs
+expression: "\npub fn main() {\n  case 5 / 2 {\n    x if x == 5 / 2 -> True\n    _ -> False\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  case 5 / 2 {
+    x if x == 5 / 2 -> True
+    _ -> False
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+import { divideInt } from "../gleam.mjs";
+
+export function main() {
+  let $ = divideInt(5, 2);
+  if ($ === (divideInt(5, 2))) {
+    let x = $;
+    return true;
+  } else {
+    return false;
+  }
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__int_remainder.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__int_remainder.snap
new file mode 100644
index 000000000..042fb1e38
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__int_remainder.snap
@@ -0,0 +1,26 @@
+---
+source: compiler-core/src/javascript/tests/case_clause_guards.rs
+expression: "\npub fn main() {\n  case 4 % 0 {\n    x if x == 4 % 0 -> True\n    _ -> False\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  case 4 % 0 {
+    x if x == 4 % 0 -> True
+    _ -> False
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+import { remainderInt } from "../gleam.mjs";
+
+export function main() {
+  let $ = remainderInt(4, 0);
+  if ($ === (remainderInt(4, 0))) {
+    let x = $;
+    return true;
+  } else {
+    return false;
+  }
+}