Skip to content

Commit cfe9b6c

Browse files
proggeramlugRalphclaude
authored
fix(runtime,codegen): bitnot ToNumber coercion + modulus zero/sign-of-zero (#5822)
* fix(runtime,codegen): bitnot ToNumber coercion + modulus zero/sign-of-zero (#5815) Two spec bugs caught by the test262 radar: **`~x` (bitwise NOT) — `js_dynamic_bitnot`** - Missing `ToNumber` coercion before `ToInt32`: `~new Boolean(true)` gave `-1` (treating the NaN-boxed pointer as `0`) instead of `~1 = -2`. Fixed by calling `js_number_coerce` before the i64 truncation, which dispatches through `valueOf`/`toPrimitive` for wrapper objects and strings. - Wrong `ToInt32` for `±Infinity`/`NaN`: Rust's saturating `f64 as i64` gives `i64::MAX` for `+Infinity` → `i64::MAX as i32 = -1` → `~(-1) = 0`, but the spec says `ToInt32(±Infinity) = 0` → `~0 = -1`. Fixed with an explicit `if is_nan || !is_finite { 0i32 }` guard. Fixes `S11.4.8_A2.2_T1`, `S11.4.8_A3_T1`, `S11.4.8_A3_T2`, `S11.4.8_A3_T3` (100% parity on `language/expressions/bitwise-not`). **`x % y` (modulus) — integer fast path in codegen** - Zero-divisor UB: `srem(x, 0)` is undefined behaviour in LLVM (ARM gives `0` silently; JS requires `NaN`). Guard added: skip the `fptosi/srem` fast path when the RHS is a literal `0` or `0.0` and fall through to `frem` (which gives `NaN` per IEEE 754). - Sign-of-zero: `srem` returns an `i64` zero and `sitofp` always produces `+0.0`, but JS requires `-0.0` when the dividend was negative (e.g. `-1 % -1 === -0`). Fixed by emitting: `if m == 0 && l < 0.0 → fneg(0.0)`. Fixes `S11.5.3_A4_T2`, `S11.5.3_A4_T4`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: rustfmt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Ralph <ralph@skelpo.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6b25b52 commit cfe9b6c

2 files changed

Lines changed: 28 additions & 4 deletions

File tree

crates/perry-codegen/src/expr/binary.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,33 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
216216
// fptosi, producing the wrong result. `is_integer_valued_expr`
217217
// only returns true when we can prove the value is a whole
218218
// number (integer literals, integer loop counters, or nested
219-
// integer arithmetic). For everything else we fall through
220-
// to the `frem` path.
219+
// integer arithmetic). A zero RHS falls through to `frem`
220+
// because srem(x,0) is UB in LLVM (on ARM the CPU silently
221+
// gives 0, but JS requires NaN for any x % 0). For everything
222+
// else we fall through to the `frem` path.
223+
let right_is_known_zero = matches!(**right, Expr::Integer(0))
224+
|| matches!(**right, Expr::Number(v) if v == 0.0);
221225
if matches!(op, BinaryOp::Mod)
222226
&& crate::type_analysis::is_integer_valued_expr(ctx, left)
223227
&& crate::type_analysis::is_integer_valued_expr(ctx, right)
228+
&& !right_is_known_zero
224229
{
225230
let l_raw = lower_expr(ctx, left)?;
226231
let r_raw = lower_expr(ctx, right)?;
227232
let blk = ctx.block();
228233
let li = blk.fptosi(DOUBLE, &l_raw, I64);
229234
let ri = blk.fptosi(DOUBLE, &r_raw, I64);
230235
let m = blk.srem(I64, &li, &ri);
231-
return Ok(blk.sitofp(I64, &m, DOUBLE));
236+
// IEEE 754: when the integer remainder is 0 and the
237+
// dividend was negative, the result must be -0.0.
238+
// srem gives 0i64 → sitofp always produces +0.0,
239+
// so correct: if m==0 && l<0 → fneg(0.0) = -0.0.
240+
let result_f = blk.sitofp(I64, &m, DOUBLE);
241+
let m_is_zero = blk.icmp_eq(I64, &m, "0");
242+
let l_neg = blk.fcmp("olt", &l_raw, "0.0");
243+
let need_neg = blk.and(I1, &m_is_zero, &l_neg);
244+
let neg_result = blk.fneg(&result_f);
245+
return Ok(blk.select(I1, &need_neg, DOUBLE, &neg_result, &result_f));
232246
}
233247

234248
let (l_raw, l_fallback_coerced) = lower_arithmetic_operand(ctx, left)?;

crates/perry-runtime/src/value/dynamic_arith.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,17 @@ pub unsafe extern "C" fn js_dynamic_bitnot(a: f64) -> f64 {
458458
);
459459
return js_nanbox_bigint(result as i64);
460460
}
461-
(!(a as i64 as i32)) as f64
461+
// Apply ToNumber first so that ~"3", ~true, ~new Boolean(true), etc.
462+
// coerce correctly before the ToInt32 truncation.
463+
let a_num = crate::builtins::js_number_coerce(a);
464+
// ES ToInt32: NaN, ±0, ±Infinity all map to 0; finite values use
465+
// C-style i64 truncation (equivalent to modulo-2^32 + sign-extend).
466+
let a_i32 = if a_num.is_nan() || !a_num.is_finite() {
467+
0i32
468+
} else {
469+
a_num as i64 as i32
470+
};
471+
(!a_i32) as f64
462472
}
463473

464474
/// Dynamic right shift: BigInt >> if either operand is BigInt, else i32 >> for numbers.

0 commit comments

Comments
 (0)