Skip to content
Open
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
46 changes: 46 additions & 0 deletions core/engine/src/builtins/typed_array/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,52 @@ fn uint8array_numeric_coercion() {
]);
}

#[test]
fn typed_array_large_values_numeric_coercion() {
run_test_actions([
// --- Uint8Array ---
// Large values that used to saturate at 255; must all wrap to 0
TestAction::run("let a = new Uint8Array([1e40, 2**63, 1e20, -(2**63), -1e20, 2**53 + 2]);"),
TestAction::assert_eq("a[0]", 0), // 1e40 → 0
TestAction::assert_eq("a[1]", 0), // 2^63 → 0
TestAction::assert_eq("a[2]", 0), // 1e20 → 0
TestAction::assert_eq("a[3]", 0), // -(2^63) → 0
TestAction::assert_eq("a[4]", 0), // -1e20 → 0
TestAction::assert_eq("a[5]", 2), // 2^53+2: lower 8 bits → 2
// Large *negative* float with non-zero lower bits: -(2^53+2) → 0xFFFFFFFE mod 256 = 254
TestAction::run("let a2 = new Uint8Array([-(2**53 + 2)]);"),
TestAction::assert_eq("a2[0]", 254),
// 2^32+1 wraps modulo 2^32 → 1
TestAction::run("let a3 = new Uint8Array([2**32 + 1]);"),
TestAction::assert_eq("a3[0]", 1),
// Boundary: 256 wraps to 0, -1 wraps to 255
TestAction::run("let a4 = new Uint8Array([256, -1]);"),
TestAction::assert_eq("a4[0]", 0),
TestAction::assert_eq("a4[1]", 255),
// --- Int8Array ---
TestAction::run("let b = new Int8Array([1e40, -(2**53 + 2), 2**32 + 1, 128, -129]);"),
TestAction::assert_eq("b[0]", 0), // 1e40 → 0
TestAction::assert_eq("b[1]", -2), // -(2^53+2) → 0xFE → -2 as i8
TestAction::assert_eq("b[2]", 1), // 2^32+1 → 1
TestAction::assert_eq("b[3]", -128), // 128 → -128 (wraps)
TestAction::assert_eq("b[4]", 127), // -129 → 127 (wraps)
// --- Uint16Array ---
TestAction::run("let c = new Uint16Array([1e40, -(2**53 + 2), 2**32 + 1, 65536, -1]);"),
TestAction::assert_eq("c[0]", 0), // 1e40 → 0
TestAction::assert_eq("c[1]", 65534), // -(2^53+2) → 0xFFFFFFFE mod 65536 = 65534
TestAction::assert_eq("c[2]", 1), // 2^32+1 → 1
TestAction::assert_eq("c[3]", 0), // 65536 → 0 (wraps)
TestAction::assert_eq("c[4]", 65535), // -1 → 65535
// --- Int16Array ---
TestAction::run("let d = new Int16Array([1e40, -(2**53 + 2), 2**32 + 1, 32768, -32769]);"),
TestAction::assert_eq("d[0]", 0), // 1e40 → 0
TestAction::assert_eq("d[1]", -2), // -(2^53+2) → 0xFFFE → -2 as i16
TestAction::assert_eq("d[2]", 1), // 2^32+1 → 1
TestAction::assert_eq("d[3]", -32768), // 32768 → -32768 (wraps)
TestAction::assert_eq("d[4]", 32767), // -32769 → 32767 (wraps)
]);
}

#[test]
fn float32array_storage() {
run_test_actions([
Expand Down
76 changes: 15 additions & 61 deletions core/engine/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{ToPrimitive, Zero};
use num_traits::ToPrimitive;
use std::{ops::Sub, sync::LazyLock};

use boa_gc::{Finalize, Trace};
Expand Down Expand Up @@ -1142,23 +1142,10 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;

// 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}

// 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
let int = number.abs().floor().copysign(number) as i64;

// 4. Let int8bit be int modulo 2^8.
let int_8_bit = int % 2i64.pow(8);

// 5. If int8bit ≥ 2^7, return 𝔽(int8bit - 2^8); otherwise return 𝔽(int8bit).
if int_8_bit >= 2i64.pow(7) {
Ok((int_8_bit - 2i64.pow(8)) as i8)
} else {
Ok(int_8_bit as i8)
}
// 2-5. ToInt32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
// Casting to i8 keeps the low 8 bits, which is int modulo 2^8 mapped
// into the signed range.
Ok(f64_to_int32(number) as i8)
}

/// `7.1.11 ToUint8 ( argument )`
Expand All @@ -1171,19 +1158,9 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;

// 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}

// 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
let int = number.abs().floor().copysign(number) as i64;

// 4. Let int8bit be int modulo 2^8.
let int_8_bit = int % 2i64.pow(8);

// 5. Return 𝔽(int8bit).
Ok(int_8_bit as u8)
// 2-5. ToUint32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
// Casting to u8 keeps the low 8 bits, which is int modulo 2^8.
Ok(f64_to_uint32(number) as u8)
}

/// `7.1.12 ToUint8Clamp ( argument )`
Expand Down Expand Up @@ -1243,23 +1220,10 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;

// 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}

// 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
let int = number.abs().floor().copysign(number) as i64;

// 4. Let int16bit be int modulo 2^16.
let int_16_bit = int % 2i64.pow(16);

// 5. If int16bit ≥ 2^15, return 𝔽(int16bit - 2^16); otherwise return 𝔽(int16bit).
if int_16_bit >= 2i64.pow(15) {
Ok((int_16_bit - 2i64.pow(16)) as i16)
} else {
Ok(int_16_bit as i16)
}
// 2-5. ToInt32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
// Casting to i16 keeps the low 16 bits, which is int modulo 2^16 mapped
// into the signed range.
Ok(f64_to_int32(number) as i16)
}

/// `7.1.9 ToUint16 ( argument )`
Expand All @@ -1272,19 +1236,9 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;

// 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}

// 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
let int = number.abs().floor().copysign(number) as i64;

// 4. Let int16bit be int modulo 2^16.
let int_16_bit = int % 2i64.pow(16);

// 5. Return 𝔽(int16bit).
Ok(int_16_bit as u16)
// 2-5. ToUint32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
// Casting to u16 keeps the low 16 bits, which is int modulo 2^16.
Ok(f64_to_uint32(number) as u16)
}

/// `7.1.15 ToBigInt64 ( argument )`
Expand Down
57 changes: 57 additions & 0 deletions core/engine/src/value/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,63 @@ fn to_int32() {
})]);
}

#[test]
fn to_small_ints_large_values() {
run_test_actions([TestAction::inspect_context(|ctx| {
let f = JsValue::new;

// |x| >= 2^63 used to saturate before the fix; must all yield 0
for x in [1e40, 2f64.powi(63), 1e20, -1e20] {
assert_eq!(f(x).to_uint8(ctx).unwrap(), 0);
assert_eq!(f(x).to_int8(ctx).unwrap(), 0);
assert_eq!(f(x).to_uint16(ctx).unwrap(), 0);
assert_eq!(f(x).to_int16(ctx).unwrap(), 0);
}

// 2^53 + 2: a large float whose lower bits are non-zero — positive case
let large_pos = 2f64.powi(53) + 2.0;
assert_eq!(f(large_pos).to_uint8(ctx).unwrap(), 2);
assert_eq!(f(large_pos).to_int8(ctx).unwrap(), 2);
assert_eq!(f(large_pos).to_uint16(ctx).unwrap(), 2);
assert_eq!(f(large_pos).to_int16(ctx).unwrap(), 2);

// -(2^53 + 2): large *negative* float with non-zero lower bits.
// Two's-complement wrap: modulo 2^32 gives 0xFFFF_FFFE → lower bytes 0xFE / 0xFFFE.
let large_neg = -(2f64.powi(53) + 2.0);
assert_eq!(f(large_neg).to_uint8(ctx).unwrap(), 254_u8);
assert_eq!(f(large_neg).to_int8(ctx).unwrap(), -2_i8);
assert_eq!(f(large_neg).to_uint16(ctx).unwrap(), 65534_u16);
assert_eq!(f(large_neg).to_int16(ctx).unwrap(), -2_i16);

// 2^32 + 1: wraps modulo 2^32 → 1 in all narrower types
let wrap32 = 2f64.powi(32) + 1.0;
assert_eq!(f(wrap32).to_uint8(ctx).unwrap(), 1);
assert_eq!(f(wrap32).to_int8(ctx).unwrap(), 1);
assert_eq!(f(wrap32).to_uint16(ctx).unwrap(), 1);
assert_eq!(f(wrap32).to_int16(ctx).unwrap(), 1);

// boundaries — u8 / i8
assert_eq!(f(256.0).to_uint8(ctx).unwrap(), 0);
assert_eq!(f(-1.0).to_uint8(ctx).unwrap(), 255);
assert_eq!(f(128.0).to_int8(ctx).unwrap(), -128);
assert_eq!(f(-129.0).to_int8(ctx).unwrap(), 127);

// boundaries — u16 / i16
assert_eq!(f(65536.0).to_uint16(ctx).unwrap(), 0);
assert_eq!(f(-1.0).to_uint16(ctx).unwrap(), 65535);
assert_eq!(f(32768.0).to_int16(ctx).unwrap(), -32768);
assert_eq!(f(-32769.0).to_int16(ctx).unwrap(), 32767);

// non-finite inputs must yield 0 for all narrow types
for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(f(x).to_uint8(ctx).unwrap(), 0);
assert_eq!(f(x).to_int8(ctx).unwrap(), 0);
assert_eq!(f(x).to_uint16(ctx).unwrap(), 0);
assert_eq!(f(x).to_int16(ctx).unwrap(), 0);
}
})]);
}

#[test]
fn to_string() {
run_test_actions([TestAction::inspect_context(|ctx| {
Expand Down
Loading