Skip to content

Commit a7bead1

Browse files
Copilotsapphi-red
andcommitted
Add ARM64 FJCVTZS instruction optimization for ToInt32 implementation
Co-authored-by: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
1 parent c483343 commit a7bead1

File tree

2 files changed

+120
-49
lines changed

2 files changed

+120
-49
lines changed

crates/oxc_ecmascript/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub use self::{
4747
string_to_number::StringToNumber,
4848
to_big_int::ToBigInt,
4949
to_boolean::ToBoolean,
50-
to_int_32::ToInt32,
50+
to_int_32::{ToInt32, ToUint32},
5151
to_integer_index::ToIntegerIndex,
5252
to_number::ToNumber,
5353
to_primitive::ToPrimitive,

crates/oxc_ecmascript/src/to_int_32.rs

Lines changed: 119 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,71 +10,142 @@ pub trait ToInt32 {
1010
impl ToInt32 for f64 {
1111
#[expect(clippy::float_cmp, clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1212
fn to_int_32(&self) -> i32 {
13-
const SIGN_MASK: u64 = 0x8000_0000_0000_0000;
14-
const EXPONENT_MASK: u64 = 0x7FF0_0000_0000_0000;
15-
const SIGNIFICAND_MASK: u64 = 0x000F_FFFF_FFFF_FFFF;
16-
const HIDDEN_BIT: u64 = 0x0010_0000_0000_0000;
17-
const PHYSICAL_SIGNIFICAND_SIZE: i32 = 52; // Excludes the hidden bit.
18-
const SIGNIFICAND_SIZE: i32 = 53;
19-
20-
const EXPONENT_BIAS: i32 = 0x3FF + PHYSICAL_SIGNIFICAND_SIZE;
21-
const DENORMAL_EXPONENT: i32 = -EXPONENT_BIAS + 1;
22-
23-
fn is_denormal(number: f64) -> bool {
24-
(number.to_bits() & EXPONENT_MASK) == 0
13+
#[cfg(target_arch = "aarch64")]
14+
{
15+
f64_to_int32_arm64(*self)
2516
}
17+
#[cfg(not(target_arch = "aarch64"))]
18+
{
19+
f64_to_int32_generic(*self)
20+
}
21+
}
22+
}
2623

27-
fn exponent(number: f64) -> i32 {
28-
if is_denormal(number) {
29-
return DENORMAL_EXPONENT;
30-
}
24+
/// Converts a 64-bit floating point number to an `i32` using [`FJCVTZS`][FJCVTZS] instruction on ARM64.
25+
///
26+
/// [FJCVTZS]: https://developer.arm.com/documentation/dui0801/h/A64-Floating-point-Instructions/FJCVTZS
27+
#[cfg(target_arch = "aarch64")]
28+
fn f64_to_int32_arm64(number: f64) -> i32 {
29+
if number.is_nan() {
30+
return 0;
31+
}
32+
let ret: i32;
33+
// SAFETY: Number is not nan so no floating-point exception should throw.
34+
unsafe {
35+
std::arch::asm!(
36+
"fjcvtzs {dst:w}, {src:d}",
37+
src = in(vreg) number,
38+
dst = out(reg) ret,
39+
)
40+
}
41+
ret
42+
}
3143

32-
let d64 = number.to_bits();
33-
let biased_e = ((d64 & EXPONENT_MASK) >> PHYSICAL_SIGNIFICAND_SIZE) as i32;
44+
/// Generic implementation of ToInt32 for non-ARM64 architectures
45+
#[cfg(not(target_arch = "aarch64"))]
46+
fn f64_to_int32_generic(number: f64) -> i32 {
47+
const SIGN_MASK: u64 = 0x8000_0000_0000_0000;
48+
const EXPONENT_MASK: u64 = 0x7FF0_0000_0000_0000;
49+
const SIGNIFICAND_MASK: u64 = 0x000F_FFFF_FFFF_FFFF;
50+
const HIDDEN_BIT: u64 = 0x0010_0000_0000_0000;
51+
const PHYSICAL_SIGNIFICAND_SIZE: i32 = 52; // Excludes the hidden bit.
52+
const SIGNIFICAND_SIZE: i32 = 53;
53+
54+
const EXPONENT_BIAS: i32 = 0x3FF + PHYSICAL_SIGNIFICAND_SIZE;
55+
const DENORMAL_EXPONENT: i32 = -EXPONENT_BIAS + 1;
56+
57+
fn is_denormal(number: f64) -> bool {
58+
(number.to_bits() & EXPONENT_MASK) == 0
59+
}
3460

35-
biased_e - EXPONENT_BIAS
61+
fn exponent(number: f64) -> i32 {
62+
if is_denormal(number) {
63+
return DENORMAL_EXPONENT;
3664
}
3765

38-
fn significand(number: f64) -> u64 {
39-
let d64 = number.to_bits();
40-
let significand = d64 & SIGNIFICAND_MASK;
66+
let d64 = number.to_bits();
67+
let biased_e = ((d64 & EXPONENT_MASK) >> PHYSICAL_SIGNIFICAND_SIZE) as i32;
4168

42-
if is_denormal(number) { significand } else { significand + HIDDEN_BIT }
43-
}
69+
biased_e - EXPONENT_BIAS
70+
}
4471

45-
fn sign(number: f64) -> i64 {
46-
if (number.to_bits() & SIGN_MASK) == 0 { 1 } else { -1 }
47-
}
72+
fn significand(number: f64) -> u64 {
73+
let d64 = number.to_bits();
74+
let significand = d64 & SIGNIFICAND_MASK;
75+
76+
if is_denormal(number) { significand } else { significand + HIDDEN_BIT }
77+
}
78+
79+
fn sign(number: f64) -> i64 {
80+
if (number.to_bits() & SIGN_MASK) == 0 { 1 } else { -1 }
81+
}
4882

49-
let number = *self;
83+
// NOTE: this also matches with negative zero
84+
if !number.is_finite() || number == 0.0 {
85+
return 0;
86+
}
5087

51-
// NOTE: this also matches with negative zero
52-
if !number.is_finite() || number == 0.0 {
88+
if number.is_finite() && number <= f64::from(i32::MAX) && number >= f64::from(i32::MIN) {
89+
let i = number as i32;
90+
if f64::from(i) == number {
91+
return i;
92+
}
93+
}
94+
95+
let exponent = exponent(number);
96+
let bits = if exponent < 0 {
97+
if exponent <= -SIGNIFICAND_SIZE {
5398
return 0;
5499
}
55100

56-
if number.is_finite() && number <= f64::from(i32::MAX) && number >= f64::from(i32::MIN) {
57-
let i = number as i32;
58-
if f64::from(i) == number {
59-
return i;
60-
}
101+
significand(number) >> -exponent
102+
} else {
103+
if exponent > 31 {
104+
return 0;
61105
}
62106

63-
let exponent = exponent(number);
64-
let bits = if exponent < 0 {
65-
if exponent <= -SIGNIFICAND_SIZE {
66-
return 0;
67-
}
107+
(significand(number) << exponent) & 0xFFFF_FFFF
108+
};
109+
110+
(sign(number) * (bits as i64)) as i32
111+
}
68112

69-
significand(number) >> -exponent
70-
} else {
71-
if exponent > 31 {
72-
return 0;
73-
}
113+
/// Converts a 64-bit floating point number to an `u32` according to the [`ToUint32`][ToUint32] algorithm.
114+
///
115+
/// [ToUint32]: https://tc39.es/ecma262/#sec-touint32
116+
pub trait ToUint32 {
117+
fn to_uint_32(&self) -> u32;
118+
}
74119

75-
(significand(number) << exponent) & 0xFFFF_FFFF
76-
};
120+
impl ToUint32 for f64 {
121+
fn to_uint_32(&self) -> u32 {
122+
self.to_int_32() as u32
123+
}
124+
}
125+
126+
#[cfg(test)]
127+
mod test {
128+
use super::*;
129+
130+
#[test]
131+
fn f64_to_int32_conversion() {
132+
assert_eq!(0.0_f64.to_int_32(), 0);
133+
assert_eq!((-0.0_f64).to_int_32(), 0);
134+
assert_eq!(f64::NAN.to_int_32(), 0);
135+
assert_eq!(f64::INFINITY.to_int_32(), 0);
136+
assert_eq!(f64::NEG_INFINITY.to_int_32(), 0);
137+
assert_eq!(((i64::from(i32::MAX) + 1) as f64).to_int_32(), i32::MIN);
138+
assert_eq!(((i64::from(i32::MIN) - 1) as f64).to_int_32(), i32::MAX);
139+
140+
// Test edge cases with maximum safe integers
141+
assert_eq!((9007199254740992.0_f64).to_int_32(), 0); // 2^53
142+
assert_eq!((-9007199254740992.0_f64).to_int_32(), 0); // -2^53
143+
}
77144

78-
(sign(number) * (bits as i64)) as i32
145+
#[test]
146+
fn f64_to_uint32_conversion() {
147+
assert_eq!(0.0_f64.to_uint_32(), 0);
148+
assert_eq!((-1.0_f64).to_uint_32(), 4294967295); // -1 as u32 is 2^32 - 1
149+
assert_eq!(f64::NAN.to_uint_32(), 0);
79150
}
80151
}

0 commit comments

Comments
 (0)