From 61a18899d081775cafd2db59623906da073dcd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 4 Jul 2026 20:13:12 +0000 Subject: [PATCH] fix(runtime): js_dynamic_mod preserves sign of zero (fmod semantics) js_dynamic_mod computed the remainder with the closed form a - (a / b).trunc() * b. That drops the sign of a zero result: -1 % -1 evaluates to -1.0 - (1.0 * -1.0) == +0.0, but JS % is C fmod, whose result takes the sign of the dividend, so -1 % -1 is -0. The same closed form also returned NaN for x % Infinity (should be x). This surfaced as a regression once a compound assignment on a separately-declared int-valued local (var x; x = -1; x %= -1) started routing through js_dynamic_mod: language/expressions/compound-assignment/mod-whitespace.js which asserts x %= -1 is -0. Rust's f64 % f64 is fmod, so replace the closed form with a % b. It matches the spec on the sign of zero, on x % Infinity == x, and on Infinity % y / x % 0 == NaN. Verified on an internal Linux sweep host: mod-whitespace passes; the compound-assignment and modulus slices are 100% (0 runtime-fails). --- crates/perry-runtime/src/value/dynamic_arith.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/perry-runtime/src/value/dynamic_arith.rs b/crates/perry-runtime/src/value/dynamic_arith.rs index 84cb2a0335..ac418b91a5 100644 --- a/crates/perry-runtime/src/value/dynamic_arith.rs +++ b/crates/perry-runtime/src/value/dynamic_arith.rs @@ -498,10 +498,15 @@ pub unsafe extern "C" fn js_dynamic_mod(a: f64, b: f64) -> f64 { } let a = dynamic_number_operand(a); let b = dynamic_number_operand(b); - // Float modulo: a - trunc(a / b) * b let a = numify_arith_operand(a); let b = numify_arith_operand(b); - a - (a / b).trunc() * b + // JS `%` is C `fmod`: the result takes the *sign of the dividend*, so + // `-1 % -1` is `-0`, not `+0`. The old `a - (a / b).trunc() * b` closed-form + // lost that (`-1.0 - 1.0 * -1.0 == +0.0`) and also returned `NaN` for + // `x % Infinity` (should be `x`). Rust's `f64 % f64` *is* `fmod`, matching + // the spec exactly on the sign of zero, `x % ±Inf`, and `±Inf % y` / `x % 0` + // → `NaN` (test262 compound-assignment `mod-whitespace`: `-0` expected). + a % b } /// Dynamic negate: -BigInt if operand is BigInt, else -f64.