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
20 changes: 12 additions & 8 deletions crates/perry-runtime/src/builtins/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub extern "C" fn js_console_log(value: JSValue) {
// Match Node/V8 console.log semantics: distinguish -0 from 0
if is_negative_zero(n) {
println!("-0");
} else if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
} else if n.fract() == 0.0 && n.abs() < INT_EXACT_FASTPATH_LIMIT {
// Print integers without decimal point
println!("{}", n as i64);
} else {
Expand Down Expand Up @@ -97,10 +97,14 @@ pub extern "C" fn js_console_log_dynamic(value: f64) {
}
} else if is_negative_zero(n) {
println!("{}-0", p);
} else if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
} else if n.fract() == 0.0 && n.abs() < INT_EXACT_FASTPATH_LIMIT {
println!("{}{}", p, n as i64);
} else {
println!("{}{}", p, n);
// Match `js_console_error_dynamic` / `js_console_warn_dynamic`: use the
// shared JS formatter (shortest round-trip + the 1e21/1e-6 exponential
// thresholds), not Rust's `f64` Display — which prints `1e21` as
// `1000000000000000000000` instead of Node's `1e+21` (#6127).
println!("{}{}", p, format_finite_number_js(n));
}
}
}
Expand Down Expand Up @@ -228,7 +232,7 @@ pub extern "C" fn js_console_log_number(value: f64) {
} else {
println!("-Infinity");
}
} else if value.fract() == 0.0 && value.abs() < (i64::MAX as f64) {
} else if value.fract() == 0.0 && value.abs() < INT_EXACT_FASTPATH_LIMIT {
println!("{}", value as i64);
} else {
println!("{}", format_finite_number_js(value));
Expand Down Expand Up @@ -274,7 +278,7 @@ pub extern "C" fn js_console_error_dynamic(value: f64) {
}
} else if is_negative_zero(n) {
eprintln!("-0");
} else if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
} else if n.fract() == 0.0 && n.abs() < INT_EXACT_FASTPATH_LIMIT {
eprintln!("{}", n as i64);
} else {
eprintln!("{}", format_finite_number_js(n));
Expand All @@ -287,7 +291,7 @@ pub extern "C" fn js_console_error_dynamic(value: f64) {
pub extern "C" fn js_console_error_number(value: f64) {
if is_negative_zero(value) {
eprintln!("-0");
} else if value.fract() == 0.0 && value.abs() < (i64::MAX as f64) {
} else if value.fract() == 0.0 && value.abs() < INT_EXACT_FASTPATH_LIMIT {
eprintln!("{}", value as i64);
} else {
eprintln!("{}", format_finite_number_js(value));
Expand Down Expand Up @@ -333,7 +337,7 @@ pub extern "C" fn js_console_warn_dynamic(value: f64) {
}
} else if is_negative_zero(n) {
eprintln!("-0");
} else if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
} else if n.fract() == 0.0 && n.abs() < INT_EXACT_FASTPATH_LIMIT {
eprintln!("{}", n as i64);
} else {
eprintln!("{}", format_finite_number_js(n));
Expand All @@ -346,7 +350,7 @@ pub extern "C" fn js_console_warn_dynamic(value: f64) {
pub extern "C" fn js_console_warn_number(value: f64) {
if is_negative_zero(value) {
eprintln!("-0");
} else if value.fract() == 0.0 && value.abs() < (i64::MAX as f64) {
} else if value.fract() == 0.0 && value.abs() < INT_EXACT_FASTPATH_LIMIT {
eprintln!("{}", value as i64);
} else {
eprintln!("{}", format_finite_number_js(value));
Expand Down
26 changes: 24 additions & 2 deletions crates/perry-runtime/src/builtins/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,28 @@ pub(crate) fn format_finite_number_js(value: f64) -> String {
}
}

/// 2^53 — the largest magnitude below which every integer is exactly
/// representable as an `f64`. `console.log` / inspect / `util.format('%d')` fast
/// paths print a whole number via a direct `as i64` cast for speed, but that is
/// only the shortest-round-trip decimal *below* this bound. At/above it several
/// integers map to one double, and the exact integer carries more significant
/// digits than V8's shortest decimal (`2**58` → `288230376151711740`, not
/// `…744`); such values must go through `format_finite_number_js`, whose Rust
/// `{}` is shortest-round-trip. Refs #6127.
pub(crate) const INT_EXACT_FASTPATH_LIMIT: f64 = 9_007_199_254_740_992.0;

/// Format a finite, integer-valued `f64` as V8 would: the fast `as i64` cast
/// below 2^53, else the shortest-round-trip formatter. Callers that have already
/// established `value.fract() == 0.0` (or truncated) use this to avoid the exact
/// vs. shortest divergence at large magnitudes. Refs #6127.
pub(crate) fn format_integral_f64(value: f64) -> String {
if value.abs() < INT_EXACT_FASTPATH_LIMIT {
(value as i64).to_string()
} else {
format_finite_number_js(value)
}
}

fn format_util_number(value: f64) -> String {
if value.is_nan() {
"NaN".to_string()
Expand Down Expand Up @@ -1055,7 +1077,7 @@ pub(crate) fn format_jsvalue(value: f64, depth: usize) -> String {
}
} else if is_negative_zero(n) {
"-0".to_string()
} else if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
} else if n.fract() == 0.0 && n.abs() < INT_EXACT_FASTPATH_LIMIT {
(n as i64).to_string()
} else {
format_finite_number_js(n)
Expand Down Expand Up @@ -1716,7 +1738,7 @@ fn format_jsvalue_for_json(value: f64, depth: usize) -> String {
}
} else if is_negative_zero(n) {
"-0".to_string()
} else if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
} else if n.fract() == 0.0 && n.abs() < INT_EXACT_FASTPATH_LIMIT {
(n as i64).to_string()
} else {
format_finite_number_js(n)
Expand Down
7 changes: 5 additions & 2 deletions crates/perry-runtime/src/builtins/formatting/util_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,11 @@ pub extern "C" fn js_util_format(arr_ptr: *const crate::array::ArrayHeader) -> f
if t == 0.0 && f.is_sign_negative() {
out.push_str("-0");
} else {
// Integer-truncated, matching Node.
out.push_str(&(t as i64).to_string());
// Integer-truncated, matching Node. Format the
// whole value shortest-round-trip so large
// magnitudes (`%d` of `2**58`) print V8's
// `…740`, not the exact `…744` (#6127).
out.push_str(&super::format_integral_f64(t));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pub(crate) use formatting::{
boxed_primitive_json_value, boxed_primitive_payload, boxed_primitive_to_string_tag,
format_finite_number_js, format_jsvalue, is_negative_zero, jsvalue_string_content,
InspectCompactGuard, InspectCustomInspectGuard, InspectDepthLimitGuard, InspectGettersGuard,
InspectShowHiddenGuard, InspectSortedGuard,
InspectShowHiddenGuard, InspectSortedGuard, INT_EXACT_FASTPATH_LIMIT,
};

pub use globals::{
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/builtins/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fn format_table_cell(value: f64) -> String {
}
} else if is_negative_zero(n) {
"-0".to_string()
} else if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
} else if n.fract() == 0.0 && n.abs() < crate::builtins::INT_EXACT_FASTPATH_LIMIT {
(n as i64).to_string()
} else {
format_finite_number_js(n)
Expand Down
9 changes: 6 additions & 3 deletions crates/perry-runtime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1449,10 +1449,13 @@ pub extern "C" fn js_number_to_locale_string(n: f64) -> *mut crate::StringHeader
// `Number.prototype.toLocaleString()` shows up to 3 fraction digits
// (Intl.NumberFormat default `maximumFractionDigits`), trailing
// zeros stripped.
let int_part = abs.trunc() as u64;
let frac = abs - abs.trunc();
// Format integer part with comma every 3 digits (en-US).
let int_str = int_part.to_string();
// Format integer part with comma every 3 digits (en-US). #6127: derive the
// digits from the f64's shortest round-trip (Rust `{}` is always positional,
// never scientific) rather than an exact `as u64` cast — the cast both
// overflows past ~1.8e19 and, in [2^53, 2^64), prints more digits than the
// shortest decimal V8 groups (`2**60` → `…847,000`, not `…846,976`).
let int_str = format!("{}", abs.trunc());
let mut grouped = String::new();
let bytes = int_str.as_bytes();
let len = bytes.len();
Expand Down
6 changes: 4 additions & 2 deletions crates/perry-runtime/src/json/stringify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,11 @@ pub(crate) unsafe fn write_number(buf: &mut String, value: f64) {
if value.is_nan() || value.is_infinite() {
// JSON has no NaN/Infinity literal; the spec serializes them as null.
buf.push_str("null");
} else if value.fract() == 0.0 && value.abs() < (i64::MAX as f64) {
} else if value.fract() == 0.0 && value.abs() < crate::builtins::INT_EXACT_FASTPATH_LIMIT {
// Fast path for in-range integers (the overwhelming majority of JSON
// numbers); identical to ECMAScript NumberToString over this range.
// numbers); identical to ECMAScript NumberToString below 2^53. Above it
// the exact integer can carry more digits than the shortest round-trip
// (`2**58`), so those fall through to `js_format_f64` in the else (#6127).
let mut itoa_buf = itoa::Buffer::new();
buf.push_str(itoa_buf.format(value as i64));
} else {
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/json/stringify_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,13 @@ pub unsafe extern "C" fn js_json_stringify_number(value: f64) -> *mut StringHead
if value.is_nan() || value.is_infinite() {
return js_string_from_bytes(b"null".as_ptr(), 4);
}
if value.fract() == 0.0 && value.abs() < (i64::MAX as f64) {
if value.fract() == 0.0 && value.abs() < crate::builtins::INT_EXACT_FASTPATH_LIMIT {
let mut itoa_buf = itoa::Buffer::new();
let s = itoa_buf.format(value as i64);
return js_string_from_bytes(s.as_ptr(), s.len() as u32);
}
// #6127: at/above 2^53 the exact integer can carry more digits than the
// shortest round-trip (`2**58`), so defer to the shortest-round-trip formatter.
let s = crate::string::js_format_f64(value);
js_string_from_bytes(s.as_ptr(), s.len() as u32)
}
Expand Down
8 changes: 7 additions & 1 deletion crates/perry-runtime/src/node_stream_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ use crate::object::ObjectHeader;
pub(super) fn push_json_number(buf: &mut String, value: f64) {
if value.is_nan() || value.is_infinite() {
buf.push_str("null");
} else if value.fract() == 0.0 && value.abs() < (i64::MAX as f64) {
} else if value.fract() == 0.0 && value.abs() < crate::builtins::INT_EXACT_FASTPATH_LIMIT {
let mut itoa_buf = itoa::Buffer::new();
buf.push_str(itoa_buf.format(value as i64));
} else if value.fract() == 0.0 {
// #6127: a large integer (`>= 2^53`) must print its shortest round-trip
// decimal in POSITIONAL notation per ECMAScript Number::toString, not the
// exact integer and not `ryu`'s scientific form (`2.88…e17`). The shared
// JS formatter handles the exponent thresholds (`1e21` → `1e+21`).
buf.push_str(&crate::string::js_format_f64(value));
} else {
let mut ryu_buf = ryu::Buffer::new();
buf.push_str(ryu_buf.format(value));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,9 @@ pub(super) unsafe fn dispatch_common(
} else {
payload
};
let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
let s = if n.fract() == 0.0
&& n.abs() < crate::builtins::INT_EXACT_FASTPATH_LIMIT
{
(n as i64).to_string()
} else {
n.to_string()
Expand Down Expand Up @@ -753,7 +755,7 @@ pub(super) unsafe fn dispatch_common(
crate::value::js_jsvalue_to_string_radix(object, radix_arg.unwrap());
return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits()));
}
let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
let s = if n.fract() == 0.0 && n.abs() < crate::builtins::INT_EXACT_FASTPATH_LIMIT {
(n as i64).to_string()
} else {
n.to_string()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ pub(super) unsafe fn dispatch_primitive(
} else {
payload
};
let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
let s = if n.fract() == 0.0
&& n.abs() < crate::builtins::INT_EXACT_FASTPATH_LIMIT
{
(n as i64).to_string()
} else {
n.to_string()
Expand Down
Loading