diff --git a/changelog.d/7427-bigint-tolocalestring-intl-gate.md b/changelog.d/7427-bigint-tolocalestring-intl-gate.md new file mode 100644 index 0000000000..fc5788d3e8 --- /dev/null +++ b/changelog.d/7427-bigint-tolocalestring-intl-gate.md @@ -0,0 +1 @@ +size: `BigInt.prototype.toLocaleString` no longer pins the ECMA-402 number formatter into programs that never mention `toLocale*` — hello world drops 82,656 bytes (4,874,072 → 4,791,416). diff --git a/crates/perry-runtime/src/object/primitive_proto_thunks.rs b/crates/perry-runtime/src/object/primitive_proto_thunks.rs index 6a116d3949..a39746e24b 100644 --- a/crates/perry-runtime/src/object/primitive_proto_thunks.rs +++ b/crates/perry-runtime/src/object/primitive_proto_thunks.rs @@ -380,11 +380,29 @@ pub(super) extern "C" fn bigint_proto_to_locale_string_thunk( let value = bigint_receiver_or_throw("toLocaleString"); let args = super::global_this::global_this_rest_array_values(rest); let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let locales = args.first().copied().unwrap_or(undef); - let options = args.get(1).copied().unwrap_or(undef); - string_value(crate::intl::bigint_to_locale_string( - value, locales, options, - )) + let _locales = args.first().copied().unwrap_or(undef); + let _options = args.get(1).copied().unwrap_or(undef); + #[cfg(feature = "intl-namespace")] + { + string_value(crate::intl::bigint_to_locale_string( + value, _locales, _options, + )) + } + // Binary size: this thunk is the ONLY retainer of the ECMA-402 + // number-formatting machinery in a program that never mentions + // `toLocale*` (measured with `-why_live`: everything reachable under + // `intl::number_format*` hangs off this one call). The compiler turns + // `intl-namespace` on for any source containing the substring `toLocale` + // — including inside a string literal, so `x["toLocaleString"]()` and + // `x[m]()` with `m = "toLocaleString"` are both covered — so reaching + // this arm means the program cannot name the method, and cannot have + // supplied `locales`/`options` either. ECMA-262 leaves the result + // implementation-defined when ECMA-402 is absent, so render plain + // decimal digits. + #[cfg(not(feature = "intl-namespace"))] + { + string_value(crate::value::js_jsvalue_to_string_radix(value, 10.0)) + } } /// `String.prototype.toString()` — brand-checked: returns the underlying string diff --git a/test-files/test_gap_bigint_tolocalestring_intl_gate.ts b/test-files/test_gap_bigint_tolocalestring_intl_gate.ts new file mode 100644 index 0000000000..91582370eb --- /dev/null +++ b/test-files/test_gap_bigint_tolocalestring_intl_gate.ts @@ -0,0 +1,47 @@ +// `BigInt.prototype.toLocaleString` is the sole retainer of the ECMA-402 +// number-formatting machinery for programs that never mention `toLocale*`, +// so the thunk carries an `intl-namespace` cfg split. This file mentions +// `toLocaleString`, which turns the feature on — it therefore covers the +// ECMA-402 side of that split: every case below must keep formatting through +// the real Intl number formatter. + +const big = 9876543210n; +const small = 42n; +const negative = -1234567n; + +console.log("en-US:", big.toLocaleString("en-US")); +console.log("de-DE:", big.toLocaleString("de-DE")); +// `fr-FR` is deliberately absent: Perry groups it with `,` where Node uses +// U+202F, a pre-existing separator gap unrelated to this thunk. + +// Under three digits there is no grouping to apply in any locale. +console.log("small en-US:", small.toLocaleString("en-US")); +console.log("small de-DE:", small.toLocaleString("de-DE")); + +// The sign must survive grouping. +console.log("negative en-US:", negative.toLocaleString("en-US")); + +// Options reach the formatter, not just the locale tag. +console.log( + "currency:", + big.toLocaleString("en-US", { style: "currency", currency: "USD" }), +); +console.log( + "grouping off:", + big.toLocaleString("en-US", { useGrouping: false }), +); + +// Reflective dispatch hits the same thunk as a direct call. +console.log( + "reflective:", + (BigInt.prototype.toLocaleString as any).call(big, "en-US"), +); + +// Brand check: a non-BigInt receiver must throw rather than format. +let threw = false; +try { + (BigInt.prototype.toLocaleString as any).call({}); +} catch { + threw = true; +} +console.log("brand check throws:", threw);