Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions crates/perry-runtime/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,12 @@ pub extern "C" fn js_math_f16round(value: f64) -> f64 {
/// Math.clz32(x) -> number — count leading zeros of 32-bit integer
#[no_mangle]
pub extern "C" fn js_math_clz32(x: f64) -> f64 {
// JS spec: convert to UInt32 first
let n = if x.is_nan() || x.is_infinite() {
// JS spec: ToUint32(x) first. `x as i64 as u32` SATURATES for |x| >= 2^63
// (Math.clz32(1e20) came out 0 instead of 1); use truncate-mod-2^32.
let n = if !x.is_finite() {
0u32
} else {
x as i64 as u32
x.trunc().rem_euclid(4_294_967_296.0) as u32
};
n.leading_zeros() as f64
}
Expand Down
59 changes: 38 additions & 21 deletions crates/perry-runtime/src/value/dynamic_arith.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,12 +524,30 @@ pub unsafe extern "C" fn js_dynamic_bitnot(a: f64) -> f64 {
// toward zero and reduce modulo 2^32. `as i64` is NOT equivalent — it
// saturates for |v| >= 2^63, so `~(1e20)` came out as `~(-1)` == 0
// instead of -1661992961 (CodeRabbit review on #5466).
let a_i32 = if a_num.is_nan() || !a_num.is_finite() {
0i32
(!dyn_to_int32(a_num)) as f64
}

/// ES ToInt32 (7.1.6): truncate toward zero, reduce modulo 2^32, reinterpret as
/// signed. NaN / ±0 / ±Infinity map to 0. `v as i64 as i32` is WRONG — Rust's
/// float→int cast SATURATES for |v| >= 2^63, so e.g. ToInt32(1e20) came out as
/// -1 instead of 1661992960 (#6079).
#[inline]
fn dyn_to_int32(v: f64) -> i32 {
if !v.is_finite() {
0
} else {
a_num.trunc().rem_euclid(4294967296.0) as u32 as i32
};
(!a_i32) as f64
(v.trunc().rem_euclid(4_294_967_296.0) as u32) as i32
}
}

/// ES ToUint32 (7.1.7): as ToInt32 but reinterpreted as unsigned.
#[inline]
fn dyn_to_uint32(v: f64) -> u32 {
if !v.is_finite() {
0
} else {
v.trunc().rem_euclid(4_294_967_296.0) as u32
}
}

/// Dynamic right shift: BigInt >> if either operand is BigInt, else i32 >> for numbers.
Expand All @@ -540,10 +558,9 @@ pub unsafe extern "C" fn js_dynamic_shr(a: f64, b: f64) -> f64 {
if both_bigint_or_throw(a, b) {
return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_shr);
}
// JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating).
// Rust `f64 as i32` saturates at i32::MAX for values >= 2^31, but JS wraps.
let ai = (a as i64) as i32;
let bi = ((b as i64) as i32) & 0x1f;
// JS ToInt32(a); ToUint32(b) & 0x1F for the shift count (#6079).
let ai = dyn_to_int32(a);
let bi = dyn_to_uint32(b) & 0x1f;
(ai >> bi) as f64
}

Expand All @@ -555,9 +572,9 @@ pub unsafe extern "C" fn js_dynamic_shl(a: f64, b: f64) -> f64 {
if both_bigint_or_throw(a, b) {
return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_shl);
}
// JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating).
let ai = (a as i64) as i32;
let bi = ((b as i64) as i32) & 0x1f;
// JS ToInt32(a); ToUint32(b) & 0x1F for the shift count (#6079).
let ai = dyn_to_int32(a);
let bi = dyn_to_uint32(b) & 0x1f;
(ai << bi) as f64
}

Expand All @@ -571,8 +588,8 @@ pub unsafe extern "C" fn js_dynamic_bitand(a: f64, b: f64) -> f64 {
if both_bigint_or_throw(a, b) {
return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_and);
}
// JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating).
(((a as i64) as i32) & ((b as i64) as i32)) as f64
// JS ToInt32 both operands (#6079).
(dyn_to_int32(a) & dyn_to_int32(b)) as f64
}

/// Dynamic bitwise OR: BigInt | if either operand is BigInt, else i32 | for numbers.
Expand All @@ -583,8 +600,8 @@ pub unsafe extern "C" fn js_dynamic_bitor(a: f64, b: f64) -> f64 {
if both_bigint_or_throw(a, b) {
return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_or);
}
// JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating).
(((a as i64) as i32) | ((b as i64) as i32)) as f64
// JS ToInt32 both operands (#6079).
(dyn_to_int32(a) | dyn_to_int32(b)) as f64
}

/// Dynamic bitwise XOR: BigInt ^ if either operand is BigInt, else i32 ^ for numbers.
Expand All @@ -595,8 +612,8 @@ pub unsafe extern "C" fn js_dynamic_bitxor(a: f64, b: f64) -> f64 {
if both_bigint_or_throw(a, b) {
return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_xor);
}
// JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating).
(((a as i64) as i32) ^ ((b as i64) as i32)) as f64
// JS ToInt32 both operands (#6079).
(dyn_to_int32(a) ^ dyn_to_int32(b)) as f64
}

/// Dynamic exponentiation: `BigInt ** BigInt` when both operands are BigInt
Expand Down Expand Up @@ -628,9 +645,9 @@ pub unsafe extern "C" fn js_dynamic_ushr(a: f64, b: f64) -> f64 {
let err = crate::error::js_typeerror_new(s);
crate::exception::js_throw(js_nanbox_pointer(err as i64));
}
// JS ToUint32 then logical shift, count masked to 5 bits.
let ai = (a as i64) as u32;
let bi = ((b as i64) as i32 as u32) & 0x1f;
// JS ToUint32(a) then logical shift; ToUint32(b) & 0x1F count (#6079).
let ai = dyn_to_uint32(a);
let bi = dyn_to_uint32(b) & 0x1f;
(ai >> bi) as f64
}

Expand Down
14 changes: 14 additions & 0 deletions test-files/test_gap_toint32_dynamic_bitops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// ToInt32/ToUint32 for |x| >= 2^63 must truncate-mod-2^32, not saturate.
// Force the dynamic (any-typed) bitwise/shift helpers + Math.clz32. #6079.
const big: any = 1e20;
const neg: any = -1e20;
const b2: any = 5e9;
const b3: any = 3e9;
console.log(Math.clz32(big), Math.clz32(1e40), Math.clz32(0), Math.clz32(-1), Math.clz32(2 ** 32));
console.log(big | 0, neg | 0, big >>> 0);
console.log(big & 0xffff, big | 1, big ^ 0);
console.log(big >> 4, big << 4, big >>> 4);
console.log(b2 | 0, b2 >>> 0, b2 & 7, b3 | 0);
// sanity: small any-typed operands unchanged
const s: any = 5;
console.log(s | 2, s & 3, s ^ 1, s << 1, s >> 1, s >>> 1, ~s);
Loading