Severity: P2 (wrong string output; value is correct)
Found while addressing a CodeRabbit note on #6123 (v0.5.1255).
What
Large integers where several integers map to the same f64 are printed as the exact integer value instead of V8's shortest round-trip decimal.
console.log(2 ** 58); // node: 288230376151711740 perry: 288230376151711744
console.log(2 ** 60); // node: 1152921504606847000 perry: 1152921504606846976
The stored double is identical (Number("0x3fffffffffffff8") === 2**58 is true, bits 4390000000000000) — only the Number → String rendering differs. Node/V8 print the shortest decimal that round-trips to the double; Perry prints the full integer, which has more significant digits than round-tripping requires.
Why
js_format_f64's integer path (magnitudes below the 1e21 exponential threshold, crates/perry-runtime/src/value/to_string.rs) renders the exact integer rather than applying the shortest-round-trip (Grisu/Ryū) algorithm that the fractional path uses. For integers < 2^53 every integer is representable so the exact form is shortest; at ≥ 2^53 the double's shortest decimal can have trailing zeros the exact integer doesn't.
Repro (characterization)
Matches node: 2**53, 2**54, 1e16, 9007199254740994. Diverges: 2**58, 2**60 (and other magnitudes whose nearest double's shortest decimal ends in zeros).
Fix direction
Route the integer branch through the same shortest-round-trip digit generator as the fractional/exponential paths (Perry already has spec_to_exponential), or apply shortest-round-trip whenever |v| >= 2^53.
Note
Number() parsing of such literals is correct (produces the right double) — this is purely the print side. Confirmed via test_gap_number_nondecimal_overflow which asserts the doubles with ===.
Confidence: high (probe + source).
Severity: P2 (wrong string output; value is correct)
Found while addressing a CodeRabbit note on #6123 (v0.5.1255).
What
Large integers where several integers map to the same
f64are printed as the exact integer value instead of V8's shortest round-trip decimal.The stored double is identical (
Number("0x3fffffffffffff8") === 2**58istrue, bits4390000000000000) — only theNumber → Stringrendering differs. Node/V8 print the shortest decimal that round-trips to the double; Perry prints the full integer, which has more significant digits than round-tripping requires.Why
js_format_f64's integer path (magnitudes below the 1e21 exponential threshold,crates/perry-runtime/src/value/to_string.rs) renders the exact integer rather than applying the shortest-round-trip (Grisu/Ryū) algorithm that the fractional path uses. For integers< 2^53every integer is representable so the exact form is shortest; at≥ 2^53the double's shortest decimal can have trailing zeros the exact integer doesn't.Repro (characterization)
Matches node:
2**53,2**54,1e16,9007199254740994. Diverges:2**58,2**60(and other magnitudes whose nearest double's shortest decimal ends in zeros).Fix direction
Route the integer branch through the same shortest-round-trip digit generator as the fractional/exponential paths (Perry already has
spec_to_exponential), or apply shortest-round-trip whenever|v| >= 2^53.Note
Number()parsing of such literals is correct (produces the right double) — this is purely the print side. Confirmed viatest_gap_number_nondecimal_overflowwhich asserts the doubles with===.Confidence: high (probe + source).