Skip to content

fix(intl): locale-aware group/decimal separators + zero-arg bigint toLocaleString grouping (#7429, #7428) - #7797

Merged
proggeramlug merged 1 commit into
mainfrom
fix/7429-7428-locale-separators
Aug 11, 2026
Merged

fix(intl): locale-aware group/decimal separators + zero-arg bigint toLocaleString grouping (#7429, #7428)#7797
proggeramlug merged 1 commit into
mainfrom
fix/7429-7428-locale-separators

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

toLocaleString now uses each locale's own digit separators, and groups when called with no arguments (#7429, #7428).

#7429 — the separator pair was a de-vs-everything-else branch. Written when de-DE was the only non-en locale under test, and duplicated at three sites. So every locale that does not group with , was wrong, not only French. Measured against Node v26.5.1 across 21 locales, Perry produced ,/. for all of them except German; the correct data is three groups — ./, for de/es/it/pt/nl/tr, a space plus , for fr/ru/pl/nb/sv/fi/cs/hu/uk, and ,/. for en/ja/ko/zh. locale_separators now returns the CLDR pair for the primary language subtag, and the three call sites share it.

The French case is the one the issue caught, and it is the sharp one: fr-FR groups with U+202F (narrow no-break space) while fr-CA uses U+00A0. Those render identically in a terminal, so the region is consulted for French and only for French. Getting it wrong is silent everywhere except a byte-for-byte oracle diff — and the first cut of this fix did get it wrong, slicing locale.get(..6) against the five-byte "fr-FR", which returns None and quietly demoted every French locale to the U+00A0 arm. The oracle diff caught it; nothing else would have.

Locales not named in the table keep the previous ,/. default, so this widens correctness without changing any locale it does not list.

#7428 — zero-argument bigint.toLocaleString() produced no grouping, while toLocaleString(undefined) was already correct. Codegen lowers the zero-arg form to Expr::DateToLocaleString, which lands in js_object_default_to_locale_string; that function has arms for numbers, Dates and Temporal values, and a BigInt fell past them into Object.prototype's "Invoke(O, 'toString')" tail — and BigInt.prototype.toString has no grouping. Any call carrying locales/options goes down the generic method-call path to the real thunk instead, so the two forms never met. That asymmetry is why the bug survived: the natural way to write a test for it (toLocaleString(undefined)) exercises the other path. A BigInt arm now formats through the same ECMA-402 machinery with the default locale.

test_gap_intl_locale_separators_7429_7428.ts covers both, asserting separators as code points rather than as formatted strings, so U+202F, U+00A0 and a plain space cannot be confused; and it exercises Intl.NumberFormat with a fractional value so a locale that got the group separator right and the decimal wrong still fails.

Verified: the new gap test passes byte-for-byte against Node v26.5.1, all 21 locales plus the Intl.NumberFormat rows match, and test_gap_intl (7) and test_gap_bigint (4) stay green. cargo test -p perry-runtime --lib is unchanged; the one intermittent failure seen during validation is gc::tests::root_words::bare_address_in_shadow_slot_survives_a_real_collection, which reproduces at 2/10 runs on clean main with these changes reverted and rebuilt — it is #7365, not this change.

Summary by CodeRabbit

  • Bug Fixes

    • Improved locale-aware number formatting with region-appropriate decimal and grouping separators.
    • Added correct French spacing and support for additional locale conventions.
    • Fixed zero-argument BigInt.prototype.toLocaleString() to apply default-locale grouping consistently.
  • Tests

    • Added coverage for separator characters, decimal/grouping output, and BigInt formatting consistency.
  • Documentation

    • Documented the updated locale separator and BigInt formatting behavior.

@proggeramlug
proggeramlug force-pushed the fix/7429-7428-locale-separators branch from eed75fb to e20e65f Compare August 10, 2026 20:26
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Locale formatting

Layer / File(s) Summary
Locale separator resolution
crates/perry-runtime/src/intl/number_format.rs
Number and BigInt formatting now use locale-specific CLDR-style grouping and decimal separators, including French regional spacing and fallback behavior.
BigInt locale formatting entry point
crates/perry-runtime/src/object/native_call_method/object_proto.rs
Zero-argument BigInt toLocaleString() calls now use the internationalized formatter with undefined locale and options.
Regression coverage and changelog
test-files/test_gap_intl_locale_separators_7429_7428.ts, changelog.d/7797-locale-separators.md
Tests cover default and explicit-undefined BigInt formatting, locale separator code points, and decimal/grouping output. The changelog records these changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BigIntReceiver
  participant object_proto
  participant bigint_to_locale_string
  participant locale_separators
  BigIntReceiver->>object_proto: toLocaleString()
  object_proto->>bigint_to_locale_string: undefined locale and options
  bigint_to_locale_string->>locale_separators: resolve locale separators
  locale_separators-->>bigint_to_locale_string: grouping and decimal separators
  bigint_to_locale_string-->>object_proto: formatted BigInt string
Loading

Possibly related PRs

  • PerryTS/perry#6853: Adds reusable locale canonicalization used by locale-related runtime code.
  • PerryTS/perry#7427: Introduces the BigInt toLocaleString paths extended by this change.

Suggested labels: bug

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes both primary fixes: locale-aware separators and zero-argument BigInt grouping.
Description check ✅ Passed The description clearly explains the changes, related issues, implementation details, regression tests, and validation results.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7429-7428-locale-separators

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
changelog.d/7797-locale-separators.md (1)

1-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the fragment limited to shipped behavior.

Retain a concise user-facing summary. Remove the implementation history, Node version, validation commands, and unrelated intermittent-test detail from lines 3-13.

Based on learnings: changelog fragments must describe the final shipped behavior as one coherent release-note entry and must not include development-slice narratives.

🤖 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 `@changelog.d/7797-locale-separators.md` around lines 1 - 13, Condense the
changelog fragment to one concise user-facing entry describing the shipped
locale-specific separators and zero-argument BigInt toLocaleString grouping
behavior. Remove implementation history, issue-analysis details, Node/version
comparisons, test and validation commands, and unrelated intermittent-test
information; retain only the final observable behavior.

Source: Learnings

🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/perry-runtime/src/intl/number_format.rs`:
- Around line 903-923: Update locale_separators in
crates/perry-runtime/src/intl/number_format.rs (lines 903-923) to parse regional
overrides and return CLDR separators for fr-CH (apostrophe grouping and "."
decimal), while preserving the existing fr-FR/fr-CA behavior and
primary-language fallback. Extend
test-files/test_gap_intl_locale_separators_7429_7428.ts (lines 33-48) with fr-CH
and the other required regional override cases to guard against regressions.

In `@crates/perry-runtime/src/object/native_call_method/object_proto.rs`:
- Around line 112-117: Update the BigInt branch in the native call path to use
the fast path only when BigInt.prototype.toLocaleString remains the intact
callable built-in; otherwise route zero-argument calls through normal property
dispatch so replacements, deletions, accessors, and non-callable values are
respected. Ensure explicit undefined follows the same override behavior as zero
arguments, and add regression coverage for replacement and deletion.

---

Nitpick comments:
In `@changelog.d/7797-locale-separators.md`:
- Around line 1-13: Condense the changelog fragment to one concise user-facing
entry describing the shipped locale-specific separators and zero-argument BigInt
toLocaleString grouping behavior. Remove implementation history, issue-analysis
details, Node/version comparisons, test and validation commands, and unrelated
intermittent-test information; retain only the final observable behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf474558-b906-4bfd-a4ce-0cb29a78f5c7

📥 Commits

Reviewing files that changed from the base of the PR and between db44b31 and e20e65f.

📒 Files selected for processing (4)
  • changelog.d/7797-locale-separators.md
  • crates/perry-runtime/src/intl/number_format.rs
  • crates/perry-runtime/src/object/native_call_method/object_proto.rs
  • test-files/test_gap_intl_locale_separators_7429_7428.ts

Comment on lines +903 to +923
match locale_lang(locale) {
// `.` group, `,` decimal.
"de" | "es" | "it" | "pt" | "nl" | "tr" | "id" | "da" | "ro" | "el" | "vi" | "ca" => {
('.', ',')
}
// Space group, `,` decimal. French splits by region: fr-FR is U+202F,
// fr-CA (and the rest of these) U+00A0.
"fr" => {
// `"fr-FR"` is five bytes; slicing `..6` returns None and silently
// demotes every French locale to the U+00A0 arm.
let region_fr = locale.eq_ignore_ascii_case("fr")
|| locale
.get(..5)
.is_some_and(|p| p.eq_ignore_ascii_case("fr-fr"));
(if region_fr { NNBSP } else { NBSP }, ',')
}
"ru" | "pl" | "nb" | "no" | "sv" | "fi" | "cs" | "sk" | "hu" | "uk" | "lv" | "lt"
| "et" | "bg" => (NBSP, ','),
// `,` group, `.` decimal — en, ja, ko, zh, he, th, and the default.
_ => (',', '.'),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Resolve separator pairs by locale region where CLDR requires it.

locale_separators maps fr-CH to NBSP and ,, but CLDR specifies apostrophe grouping and . decimal for fr_CH. This makes both Number and BigInt locale formatting incorrect for that locale. (unicode.org)

  • crates/perry-runtime/src/intl/number_format.rs#L903-L923: parse and apply regional separator overrides beyond fr-FR and fr-CA, including fr-CH.
  • test-files/test_gap_intl_locale_separators_7429_7428.ts#L33-L48: add fr-CH and other regional overrides to prevent primary-language fallback regressions.
📍 Affects 2 files
  • crates/perry-runtime/src/intl/number_format.rs#L903-L923 (this comment)
  • test-files/test_gap_intl_locale_separators_7429_7428.ts#L33-L48
🤖 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/intl/number_format.rs` around lines 903 - 923,
Update locale_separators in crates/perry-runtime/src/intl/number_format.rs
(lines 903-923) to parse regional overrides and return CLDR separators for fr-CH
(apostrophe grouping and "." decimal), while preserving the existing fr-FR/fr-CA
behavior and primary-language fallback. Extend
test-files/test_gap_intl_locale_separators_7429_7428.ts (lines 33-48) with fr-CH
and the other required regional override cases to guard against regressions.

Comment on lines +112 to +117
#[cfg(feature = "intl-namespace")]
if jsval.is_bigint() {
let undef = f64::from_bits(crate::value::TAG_UNDEFINED);
let s = crate::intl::bigint_to_locale_string(receiver, undef, undef);
return f64::from_bits(JSValue::string_ptr(s).bits());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline crates/perry-runtime/src/object/native_call_method/object_proto.rs \
  --items all --match js_object_default_to_locale_string

rg -n -C 5 --glob '*.rs' \
  'DateToLocaleString|js_object_default_to_locale_string|bigint_proto_to_locale_string_thunk|builtin_proto_user_method' \
  crates

rg -n -C 4 --glob '*.ts' \
  'BigInt\.prototype\.toLocaleString|toLocaleString\(' \
  test-files

Repository: PerryTS/perry

Length of output: 49783


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- object_proto.rs ---'
sed -n '60,180p' crates/perry-runtime/src/object/native_call_method/object_proto.rs

printf '%s\n' '--- common_methods.rs ---'
sed -n '430,515p' crates/perry-runtime/src/object/native_call_method/common_methods.rs

printf '%s\n' '--- proto_dispatch.rs ---'
sed -n '1,260p' crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs

printf '%s\n' '--- BigInt thunk and installation ---'
sed -n '80,125p' crates/perry-runtime/src/object/primitive_proto_thunks.rs
sed -n '360,400p' crates/perry-runtime/src/object/primitive_proto_thunks.rs

printf '%s\n' '--- override helper ---'
sed -n '340,410p' crates/perry-runtime/src/object/native_call_method.rs

printf '%s\n' '--- relevant tests ---'
rg -n -C 5 --glob '*.ts' \
  'BigInt\.prototype\.toLocaleString|toLocaleString\s*=\s*|delete\s+BigInt\.prototype' \
  test-files

Repository: PerryTS/perry

Length of output: 27430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 \
  'pub unsafe fn js_native_call_method|unsafe fn js_native_call_method|fn js_native_call_method' \
  crates/perry-runtime/src/object/native_call_method.rs \
  crates/perry-runtime/src/object/native_call_method/*.rs

sed -n '300,360p' crates/perry-hir/src/lower/expr_call/url_date_instance.rs

rg -n -C 10 \
  'primitive_proto_method_value|try_dispatch.*proto|js_native_call_method\(' \
  crates/perry-runtime/src/object/native_call_method.rs \
  crates/perry-runtime/src/object/native_call_method/*.rs

python3 - <<'PY'
from pathlib import Path

lowering = Path("crates/perry-hir/src/lower/expr_call/url_date_instance.rs").read_text()
runtime = Path("crates/perry-runtime/src/object/native_call_method/object_proto.rs").read_text()
common = Path("crates/perry-runtime/src/object/native_call_method/common_methods.rs").read_text()

checks = {
    "zero_arg_lowering": 'if args.is_empty()' in lowering and
        'Expr::DateToLocaleString(Box::new(date_expr))' in lowering,
    "bigint_fast_path": 'if jsval.is_bigint()' in runtime and
        'bigint_to_locale_string(receiver, undef, undef)' in runtime,
    "generic_bigint_exemption": '"toLocaleString" if !jsval.is_bigint()' in common,
}
for name, value in checks.items():
    print(f"{name}={value}")
if not all(checks.values()):
    raise SystemExit("expected dispatch structure not found")
PY

Repository: PerryTS/perry

Length of output: 49847


Preserve BigInt.prototype.toLocaleString dispatch.

This fast path bypasses BigInt.prototype.toLocaleString, so replacements, deletions, accessors, and non-callable values have no effect. Route the zero-argument form through normal property dispatch when the built-in method is not intact. Add regression tests for replacement and deletion. Ensure zero-argument and explicit-undefined calls have identical override behavior.

🤖 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/native_call_method/object_proto.rs` around
lines 112 - 117, Update the BigInt branch in the native call path to use the
fast path only when BigInt.prototype.toLocaleString remains the intact callable
built-in; otherwise route zero-argument calls through normal property dispatch
so replacements, deletions, accessors, and non-callable values are respected.
Ensure explicit undefined follows the same override behavior as zero arguments,
and add regression coverage for replacement and deletion.

@proggeramlug
proggeramlug merged commit bec70d6 into main Aug 11, 2026
1 of 18 checks passed
@proggeramlug
proggeramlug deleted the fix/7429-7428-locale-separators branch August 11, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant