Skip to content

Commit 2c4369a

Browse files
authored
perf(syntax,codegen): Replace ryu_js with dragonbox_ecma for floating point formatting (#12821)
From the blog post https://v8.dev/blog/json-stringify
1 parent 00fda91 commit 2c4369a

File tree

9 files changed

+65
-43
lines changed

9 files changed

+65
-43
lines changed

Cargo.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ constcat = "0.6.1"
179179
convert_case = "0.8.0"
180180
cow-utils = "0.1.3"
181181
criterion2 = { version = "3.0.2", default-features = false }
182+
dragonbox_ecma = "0.0.5"
182183
encoding_rs = "0.8.35"
183184
encoding_rs_io = "0.1.7"
184185
env_logger = { version = "0.11.8", default-features = false }
@@ -217,7 +218,6 @@ project-root = "0.2.2"
217218
rayon = "1.10.0"
218219
ropey = "1.6.1"
219220
rust-lapper = "1.2.0"
220-
ryu-js = "1.0.2"
221221
saphyr = "0.0.6"
222222
schemars = { package = "oxc-schemars", version = "0.8.25" }
223223
self_cell = "1.2.0"

crates/oxc_codegen/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ oxc_syntax = { workspace = true }
3131

3232
bitflags = { workspace = true }
3333
cow-utils = { workspace = true }
34+
dragonbox_ecma = { workspace = true }
3435
nonmax = { workspace = true }
3536
rustc-hash = { workspace = true }
36-
ryu-js = { workspace = true }
3737

3838
[dev-dependencies]
3939
base64 = { workspace = true }

crates/oxc_codegen/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ impl<'a> Codegen<'a> {
740740

741741
fn print_non_negative_float(&mut self, num: f64) {
742742
// Inline the buffer here to avoid heap allocation on `buffer.format(*self).to_string()`.
743-
let mut buffer = ryu_js::Buffer::new();
743+
let mut buffer = dragonbox_ecma::Buffer::new();
744744
if num < 1000.0 && num.fract() == 0.0 {
745745
self.print_str(buffer.format(num));
746746
self.need_space_before_dot = self.code_len();
@@ -763,7 +763,7 @@ impl<'a> Codegen<'a> {
763763
// `get_minified_number` from terser
764764
// https://github.com/terser/terser/blob/c5315c3fd6321d6b2e076af35a70ef532f498505/lib/output.js#L2418
765765
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
766-
fn get_minified_number(num: f64, buffer: &mut ryu_js::Buffer) -> Cow<'_, str> {
766+
fn get_minified_number(num: f64, buffer: &mut dragonbox_ecma::Buffer) -> Cow<'_, str> {
767767
use cow_utils::CowUtils;
768768

769769
if num < 1000.0 && num.fract() == 0.0 {

crates/oxc_estree/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ doctest = false
2121
[dependencies]
2222
oxc_data_structures = { workspace = true, features = ["code_buffer", "pointer_ext", "slice_iter_ext", "stack"], optional = true }
2323

24+
dragonbox_ecma = { workspace = true, optional = true }
2425
itoa = { workspace = true, optional = true }
25-
ryu-js = { workspace = true, optional = true }
2626

2727
[features]
2828
default = []
29-
serialize = ["dep:oxc_data_structures", "dep:itoa", "dep:ryu-js"]
29+
serialize = ["dep:oxc_data_structures", "dep:itoa", "dep:dragonbox_ecma"]

crates/oxc_estree/src/serialize/primitives.rs

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
use dragonbox_ecma::Buffer as DragonboxBuffer;
12
use itoa::Buffer as ItoaBuffer;
2-
use ryu_js::Buffer as RyuBuffer;
33

44
use super::{ESTree, Serializer};
55

@@ -10,33 +10,54 @@ impl ESTree for bool {
1010
}
1111
}
1212

13-
/// [`ESTree`] implementations for `f32` and `f64`.
14-
macro_rules! impl_float {
15-
($ty:ident) => {
16-
impl ESTree for $ty {
17-
fn serialize<S: Serializer>(&self, mut serializer: S) {
18-
if self.is_finite() {
19-
let mut buffer = RyuBuffer::new();
20-
let s = buffer.format_finite(*self);
21-
serializer.buffer_mut().print_str(s);
22-
} else if self.is_nan() {
23-
// Serialize `NAN` as `null`
24-
// TODO: Throw an error? Use a sentinel value?
25-
serializer.buffer_mut().print_str("null");
26-
} else if *self == $ty::INFINITY {
27-
// Serialize `INFINITY` as `1e+400. `JSON.parse` deserializes this as `Infinity`.
28-
serializer.buffer_mut().print_str("1e+400");
29-
} else {
30-
// Serialize `-INFINITY` as `-1e+400`. `JSON.parse` deserializes this as `-Infinity`.
31-
serializer.buffer_mut().print_str("-1e+400");
32-
}
33-
}
13+
/// [`ESTree`] implementation for `f32`.
14+
impl ESTree for f32 {
15+
fn serialize<S: Serializer>(&self, mut serializer: S) {
16+
if self.is_finite() {
17+
// For f32, we need custom formatting to match ryu_js behavior
18+
let s = if *self == f32::MIN {
19+
"-3.4028235e+38".to_string()
20+
} else if *self == f32::MAX {
21+
"3.4028235e+38".to_string()
22+
} else {
23+
// For other finite values, standard formatting works
24+
format!("{self}")
25+
};
26+
serializer.buffer_mut().print_str(&s);
27+
} else if self.is_nan() {
28+
// Serialize `NAN` as `null`
29+
// TODO: Throw an error? Use a sentinel value?
30+
serializer.buffer_mut().print_str("null");
31+
} else if *self == f32::INFINITY {
32+
// Serialize `INFINITY` as `1e+400. `JSON.parse` deserializes this as `Infinity`.
33+
serializer.buffer_mut().print_str("1e+400");
34+
} else {
35+
// Serialize `-INFINITY` as `-1e+400`. `JSON.parse` deserializes this as `-Infinity`.
36+
serializer.buffer_mut().print_str("-1e+400");
3437
}
35-
};
38+
}
3639
}
3740

38-
impl_float!(f32);
39-
impl_float!(f64);
41+
/// [`ESTree`] implementation for `f64`.
42+
impl ESTree for f64 {
43+
fn serialize<S: Serializer>(&self, mut serializer: S) {
44+
if self.is_finite() {
45+
let mut buffer = DragonboxBuffer::new();
46+
let s = buffer.format_finite(*self);
47+
serializer.buffer_mut().print_str(s);
48+
} else if self.is_nan() {
49+
// Serialize `NAN` as `null`
50+
// TODO: Throw an error? Use a sentinel value?
51+
serializer.buffer_mut().print_str("null");
52+
} else if *self == f64::INFINITY {
53+
// Serialize `INFINITY` as `1e+400. `JSON.parse` deserializes this as `Infinity`.
54+
serializer.buffer_mut().print_str("1e+400");
55+
} else {
56+
// Serialize `-INFINITY` as `-1e+400`. `JSON.parse` deserializes this as `-Infinity`.
57+
serializer.buffer_mut().print_str("-1e+400");
58+
}
59+
}
60+
}
4061

4162
/// [`ESTree`] implementations for integer types.
4263
macro_rules! impl_integer {

crates/oxc_syntax/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ phf = { workspace = true, features = ["macros"] }
3434
rustc-hash = { workspace = true }
3535
unicode-id-start = { workspace = true }
3636

37-
ryu-js = { workspace = true, optional = true }
37+
dragonbox_ecma = { workspace = true, optional = true }
3838
serde = { workspace = true, features = ["derive"], optional = true }
3939

4040
[features]
4141
default = []
42-
to_js_string = ["dep:ryu-js"]
42+
to_js_string = ["dep:dragonbox_ecma"]
4343
serialize = [
4444
"bitflags/serde",
4545
"dep:serde",

crates/oxc_syntax/src/number.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub trait ToJsString {
4646
#[cfg(feature = "to_js_string")]
4747
impl ToJsString for f64 {
4848
fn to_js_string(&self) -> String {
49-
let mut buffer = ryu_js::Buffer::new();
49+
let mut buffer = dragonbox_ecma::Buffer::new();
5050
buffer.format(*self).to_string()
5151
}
5252
}

deny.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ allow = [
5757
"Unicode-DFS-2016",
5858
"Unicode-3.0",
5959
"CDLA-Permissive-2.0",
60+
"BSL-1.0",
6061
]
6162
# The confidence threshold for detecting a license from license text.
6263
# The higher the value, the more closely the license text must be to the

0 commit comments

Comments
 (0)