Skip to content

toString(radix) above 2^53 emits exact digits, not V8's shortest round-trip ((1e21).toString(36) → 5v1j4f4ds7c4ks vs 5v1j4f4ds7c000) #9725

Description

@proggeramlug

Number.prototype.toString(radix) for a non-power-of-two radix emits the double's exact digits above 2^53, where V8 emits the shortest round-trip form. Static and dynamic call sites are both affected, so this is the radix formatter itself, not a dispatch divergence.

const rows: [string, number, number][] = [
  ["255r36", 255, 36], ["1e15r36", 1e15, 36], ["2**53r36", 9007199254740992, 36],
  ["1e21r36", 1e21, 36], ["1e21r7", 1e21, 7], ["1e30r36", 1e30, 36],
];
for (const [l, n, r] of rows) console.log(l + "=" + n.toString(r));
node 26.5.1 perry
(255).toString(36) 73 73
(1e15).toString(36) 9ugxnorjls 9ugxnorjls
(2**53).toString(36) 2gosa7pa2gw 2gosa7pa2gw
(1e21).toString(36) 5v1j4f4ds7c000 5v1j4f4ds7c4ks
(1e21).toString(7) 5135235413265003022600000 5135235413265003023621156
(1e30).toString(36) 2oy99wnkl1a000000000 2oy99wnkl1a848gwks0g

Everything at or below 2^53 agrees. Above it the two disagree in the trailing digits: node stops at the last digit the shortest round-trip decimal justifies and pads with zeros, while perry keeps emitting the exact binary value's digits.

Power-of-two radices are unaffected — (1e21).toString(16) is 3635c9adc5dea00000 on both — which is what you would expect if the culprit is the general-radix digit loop rather than the value.

The mechanism is the same one INT_EXACT_FASTPATH_LIMIT documents for base 10 (crates/perry-runtime/src/builtins/formatting.rs): at and above 2^53 several integers map to one double, and the exact integer carries more significant digits than the shortest decimal that round-trips. Base 10 already routes through the shortest-round-trip formatter above that bound; the general-radix path (double_to_radix_string / js_jsvalue_to_string_radix) appears not to apply the equivalent cut.

Low severity on its own, same as #9713 — it matters where the text is compared, hashed, or parsed. Found while building the regression fixture for #9713; the two are independent (this one reproduces with a plain static n.toString(36) and #9713's fix does not touch it), so the #9713 fixture deliberately keeps its radix checks at or below 2^53.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions