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
1 change: 1 addition & 0 deletions changelog.d/7427-bigint-tolocalestring-intl-gate.md
Original file line number Diff line number Diff line change
@@ -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).
28 changes: 23 additions & 5 deletions crates/perry-runtime/src/object/primitive_proto_thunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment on lines 380 to +404

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A100 -B10 \
  'fn global_this_rest_array_values\b|pub\(super\) fn global_this_rest_array_values\b' \
  crates/perry-runtime/src

rg -n -C4 \
  'global_this_rest_array_values|arena_alloc_gc|gc_|collect|js_get_property' \
  crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Locate helper and target file"
fd -a 'primitive_proto_thunks\.rs|global_this\.rs' crates/perry-runtime/src | sed 's#^\./##'
echo

echo "## primitive_proto_thunks.rs outline around toLocaleString"
ast-grep outline crates/perry-runtime/src/object/primitive_proto_thunks.rs --match 'toLocaleString' --view expanded 2>/dev/null || true
echo
sed -n '340,425p' crates/perry-runtime/src/object/primitive_proto_thunks.rs | nl -ba -v340
echo

echo "## Search for global_this_rest_array_values definitions/usages"
rg -n -C8 'global_this_rest_array_values' crates/perry-runtime/src
echo

echo "## Search for related array conversion helpers"
rg -n -C8 'global_this.*array|rest_array|rest_array_values|array_values' crates/perry-runtime/src/object crates/perry-runtime/src/global crates/perry-runtime/src/lib.rs 2>/dev/null || true

Repository: PerryTS/perry

Length of output: 501


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## primitive_proto_thunks.rs lines 340-425"
sed -n '340,425p' crates/perry-runtime/src/object/primitive_proto_thunks.rs
echo

echo "## global_this.rs helper definition lines 1-220"
sed -n '1,220p' crates/perry-runtime/src/object/global_this.rs
echo

echo "## global_this_rest_array_values occurrences"
rg -n -C8 'global_this_rest_array_values' crates/perry-runtime/src
echo

echo "## Allocation/collection/global_this_rest_array_values occurrences in object module"
rg -n -C4 'global_this_rest_array_values|gc_check_trigger|js_gc_memory_pressure|gc_check|js_gc_step|js_gc_collect|js_gc_memory_pressure|arena_alloc_gc|gc_alloc|js_get_property|js_call|js_new|js_method|js_string|js_array|js_push|js_shadow|js_heap|js_value_to' crates/perry-runtime/src/object/primitive_proto_thunks.rs crates/perry-runtime/src/object/global_this.rs
echo

echo "## List relevant symbols in object/global_this.rs"
ast-grep outline crates/perry-runtime/src/object/global_this.rs --view expanded 2>/dev/null || true

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Exact definition files mentioning global_this_rest_array_values"
rg -n -l 'global_this_rest_array_values' crates/perry-runtime/src | while read -r f; do
  echo "##### $f"
done

echo "## primitive_proto_thunks imports/top and bigint_receiver_or_throw"
sed -n '1,80p' crates/perry-runtime/src/object/primitive_proto_thunks.rs
echo
sed -n '200,260p' crates/perry-runtime/src/object/primitive_proto_thunks.rs
echo

echo "## Search definition in likely helper/path name"
rg -n -C20 'global_this_rest_array_values|to_vec|rest\)' crates/perry-runtime/src/object/global_this.rs crates/perry-runtime/src/object/global_this/* 2>/dev/null | head -n 300
echo

echo "## Small inductive call-graph probe for global_this_rest_array_values"
python3 - <<'PY'
import pathlib, re
roots = []
for p in pathlib.Path('crates/perry-runtime/src').rglob('*.rs'):
    s = p.read_text(errors='ignore')
    if 'global_this_rest_array_values' in s:
        for m in re.finditer(r'(pub\s*\([^)]*\)(?:\s+static|extern\s+"C")?(?:\s+async\s+)?\s*fn\s+global_this_rest_array_values\b)', s):
            line = s[:m.start()].count('\n') + 1
            # gather next 220 lines bounded by braces roughly
            body = s[m.start():].splitlines()
            print(f"{p}:{line}")
            for i, line_s in enumerate(body[:180], 1):
                print(f"{i:3}: {line_s}")
PY

Repository: PerryTS/perry

Length of output: 43334


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## gc_check_trigger and allocation trigger implementation"
rg -n -C12 'fn gc_check_trigger|gc_check_trigger\(|js_gc_memory_pressure|GcTrigger' crates/perry-runtime/src | head -n 400
echo

echo "## Value/array allocation/pinning references in runtime"
rg -n -C6 'fn js_gc_memory_pressure|fn gc_check_trigger|pub fn js_gc_memory_pressure|pub extern "C" fn js_gc_memory_pressure|fn gc_alloc|pub extern "C" fn js_gc_alloc|fn gc_check|GC_UNSAFE_Z|is_gc_safe|unsafe_zone|GcUnsafe' crates/perry-runtime/src | head -n 450
echo

echo "## js_array_get_f64 definition"
rg -n -C8 'fn js_array_get_f64|extern "C" fn js_array_get_f64|pub extern "C" fn js_array_get_f64' crates/perry-runtime/src/array.rs crates/perry-runtime/src | head -n 220
echo

echo "## js_array_length definition"
rg -n -C8 'fn js_array_length|extern "C" fn js_array_length|pub extern "C" fn js_array_length' crates/perry-runtime/src/array.rs crates/perry-runtime/src | head -n 220

Repository: PerryTS/perry

Length of output: 50369


Root value around global_this_rest_array_values(rest).

global_this_rest_array_values(rest) allocates a Vec<f64> and can reach allocation-point GC. Root value before that call and reload it before both formatting branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/primitive_proto_thunks.rs` around lines 380 -
404, In the BigInt toLocaleString thunk, root the receiver value before calling
global_this_rest_array_values(rest), since that call may trigger GC. After
collecting the arguments, reload the rooted value and use the reloaded value in
both the intl and non-intl formatting branches.

Sources: Coding guidelines, Learnings

}
}

/// `String.prototype.toString()` — brand-checked: returns the underlying string
Expand Down
47 changes: 47 additions & 0 deletions test-files/test_gap_bigint_tolocalestring_intl_gate.ts
Original file line number Diff line number Diff line change
@@ -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);
Loading