Skip to content

fix(intl): #5581 — add Intl.NumberFormat formatRange/formatRangeToParts#5771

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5581-numberformat
Jun 28, 2026
Merged

fix(intl): #5581 — add Intl.NumberFormat formatRange/formatRangeToParts#5771
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5581-numberformat

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Intl.NumberFormat.prototype.formatRange and formatRangeToParts (the Intl.NumberFormat-v3 methods) were not implemented, so every test262 case touching them threw formatRange is not a function. This adds both.

Approach

  • Installed as non-bound prototype methods (for reflection — prop-desc/length/name) plus own this-based instance properties. Native Intl method dispatch in Perry resolves instance methods from own props, not the static prototype, so a prototype-only install isn't callable as nf.formatRange(...).
  • The own instance closures are this-based, not bound: a detached nf.formatRange reference loses this and the receiver guard throws a TypeError, matching the spec's plain (non-bound) prototype method — formatRange/invoked-as-func.js.
  • Range engine is a best-effort PartitionNumberRangePattern: undefined endpoints → TypeError, NaN endpoints → RangeError; mathematically-equal endpoints collapse to a single value; distinct endpoints that round to the same string render approximate (~); otherwise the two renderings join with an en dash, parts tagged startRange/endRange/shared.

Out of scope

Full ICU field-collapsing and locale range patterns are not reproduced, so the three exact-output cases (formatRange/{en-US,pt-PT}, formatRangeToParts/en-US) remain failing. These need a real ICU range formatter.

Validation

Against the pinned test262 corpus (scripts/test262_subset.py, sha 4249661):

  • intl402/NumberFormat: 83 → 71 failing (12 fixed: invoked-as-func / length / name / prop-desc / nan-arguments-throws / x-greater-than-y-not-throws for both methods).
  • New failing set is a strict subset of the prior one — no regressions.
  • 0 compile failures.

Refs #5581.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for Intl.NumberFormat range formatting, including formatRange and formatRangeToParts.
    • Improved Intl constructor setup so range-format methods are available on number format instances.
  • Bug Fixes
    • Range formatting now handles matching values more consistently, including identical numeric values and signed zero.
    • Added clearer output for approximate ranges when formatted endpoints differ only in representation.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Intl.NumberFormat.prototype.formatRange and formatRangeToParts to the runtime. A new shared install_constructor helper in intl/install.rs centralises Intl constructor/prototype wiring. number_format.rs gains endpoint validation, string-collapsing logic, and typed-parts construction. The thunks and re-exports are wired into intl.rs at both the instance and prototype level.

Changes

Intl.NumberFormat formatRange / formatRangeToParts

Layer / File(s) Summary
Shared Intl constructor installation helper
crates/perry-runtime/src/intl.rs, crates/perry-runtime/src/intl/install.rs
New install.rs submodule with install_constructor: allocates ctor closure, builds prototype with constructor back-reference, installs methods/getter-only accessors/Symbol.toStringTag, registers supportedLocalesOf, and assigns onto the namespace object. intl.rs adds the submodule import.
formatRange / formatRangeToParts core logic
crates/perry-runtime/src/intl/number_format.rs
Adds number_range_endpoints (undefined/NaN validation), range_endpoints_equal (f64 equality including ±0), number_format_range_value (collapse/approximate/en-dash string output), number_format_range_parts_value (source-tagged typed parts), and the two extern "C" thunks.
Thunk wiring and prototype/instance installation
crates/perry-runtime/src/intl.rs
Re-exports new thunks, installs formatRange/formatRangeToParts as own instance properties in make_instance, and adds them to the Intl.NumberFormat prototype method list in install_intl_namespace.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5512: Modifies the same Intl constructor installation plumbing (install_constructor patterns) in intl.rs.
  • PerryTS/perry#5605: Touches Intl.NumberFormat make_instance/constructor wiring in the same instantiation flow.
  • PerryTS/perry#5728: Changes the shared install_constructor getter-only prototype infrastructure directly connected to the new install.rs module.

Poem

🐇 Hop, hop — the numbers dance in range!
From start to end, with en-dashes so strange,
~ for approximation, shared when equal they meet,
The prototype is wired, the thunks are complete.
A rabbit rejoices: the format is sweet! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding Intl.NumberFormat range methods.
Description check ✅ Passed The description covers the key details and validation, but it omits the template's explicit Changes and Related issue sections.
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.
✨ 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 worktree-fix-5581-numberformat

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.

`Intl.NumberFormat.prototype.formatRange` and `formatRangeToParts` (the
Intl.NumberFormat-v3 methods) were missing, so every test262 case touching
them threw "formatRange is not a function".

Add both as non-bound prototype methods plus own this-based instance
properties (native Intl method dispatch resolves from own props, not the
static prototype). Using a this-based — rather than bound — closure means a
detached `nf.formatRange` reference loses `this` and the receiver guard
throws a TypeError, matching the spec's plain prototype method
(formatRange/invoked-as-func.js).

The range engine is a best-effort PartitionNumberRangePattern: undefined
endpoints are a TypeError and NaN endpoints a RangeError; mathematically
equal endpoints collapse to a single value; distinct endpoints that round
to the same string render approximate ("~"); otherwise the two renderings
join with an en dash, with parts tagged startRange/endRange/shared. The
exact ICU field-collapsing / locale range pattern is not reproduced, so
the three exact-output cases (formatRange/{en-US,pt-PT}, formatRangeToParts
/en-US) remain out of scope.

Split the generic `install_constructor` helper into a new `intl/install.rs`
child module so `intl.rs` stays under the 2000-line file-size gate.

Validated against the pinned test262 corpus: intl402/NumberFormat goes
from 83 -> 71 failing (12 fixed: invoked-as-func/length/name/prop-desc/
nan-arguments-throws/x-greater-than-y-not-throws for both methods), the new
failing set is a strict subset of the prior one (no regressions), and 0
compile failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug force-pushed the worktree-fix-5581-numberformat branch from 5c5f7a9 to d12d00d Compare June 28, 2026 18:43

@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: 1

🧹 Nitpick comments (1)
crates/perry-runtime/src/intl.rs (1)

1772-1775: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update this comment to match the instance shadowing behavior.

Line 1775 says these methods are installed “on the prototype only,” but lines 1075-1090 also install own non-bound instance properties for Perry dispatch. Keep the “not bound” warning, but remove the “prototype only” claim.

Proposed comment fix
-            // and the `this_intl_object` guard throws, so they are installed on
-            // the prototype only — never as own bound instance functions.
+            // and the `this_intl_object` guard throws. Instances also shadow
+            // these with own non-bound closures for Perry's native dispatch,
+            // but never as own bound instance functions.
🤖 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.rs` around lines 1772 - 1775, The comment near
the Intl range-format methods is outdated because it claims they are installed
“on the prototype only,” but the implementation in Intl.NumberFormat also adds
own non-bound instance properties for Perry dispatch. Update the comment around
the formatRange/formatRangeToParts setup to keep the
detached-reference/this_intl_object warning while removing the “prototype only”
wording and reflecting the instance-shadowing behavior.
🤖 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 1723-1728: The early equality collapse in range_endpoints_equal
and the related fast path in the range formatting flow are incorrectly bypassing
the approximate form for equal endpoints. Update the range handling in
number_format.rs so formatRange and formatRangeToParts still go through the
approximate rendering path when the two endpoints are equal, ensuring
formatRangeToParts includes approximatelySign instead of returning a single
collapsed value.

---

Nitpick comments:
In `@crates/perry-runtime/src/intl.rs`:
- Around line 1772-1775: The comment near the Intl range-format methods is
outdated because it claims they are installed “on the prototype only,” but the
implementation in Intl.NumberFormat also adds own non-bound instance properties
for Perry dispatch. Update the comment around the formatRange/formatRangeToParts
setup to keep the detached-reference/this_intl_object warning while removing the
“prototype only” wording and reflecting the instance-shadowing behavior.
🪄 Autofix (Beta)

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: d516a2cc-33b6-409d-bd69-c5e35c6df3fd

📥 Commits

Reviewing files that changed from the base of the PR and between 5d47364 and d12d00d.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/intl.rs
  • crates/perry-runtime/src/intl/install.rs
  • crates/perry-runtime/src/intl/number_format.rs

Comment on lines +1723 to +1728
/// Two endpoints are "mathematically equal" for range-collapse purposes when
/// they are the same Number — including `+0`/`-0`, which compare equal — so the
/// range renders as a single value rather than the approximate (`~`) form.
fn range_endpoints_equal(x: f64, y: f64) -> bool {
x == y
}

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

# Map the relevant file and inspect the surrounding range-formatting logic.
ast-grep outline crates/perry-runtime/src/intl/number_format.rs --view expanded || true

echo
echo '--- lines around 1723-1785 ---'
sed -n '1700,1805p' crates/perry-runtime/src/intl/number_format.rs | cat -n

echo
echo '--- search for range_endpoints_equal and approximate handling ---'
rg -n "range_endpoints_equal|approximatelySign|FormatApproximately|formatRange|formatRangeToParts|range_parts_to_js_array" crates/perry-runtime/src/intl/number_format.rs

Repository: PerryTS/perry

Length of output: 10970


🌐 Web query:

ECMA-402 NumberFormat formatRange identical endpoints approximate FormatApproximately identical endpoints spec

💡 Result:

In the ECMA-402 specification, the Intl.NumberFormat.prototype.formatRange method handles cases where the start and end values of a range result in the same formatted string (either because the inputs are identical or because they round to the same value under the formatter's settings) by collapsing the range into a single value [1][2][3]. When this collapse occurs, the output may be prefixed with an "approximately equals" symbol (e.g., "~$3") [1][2][3]. The insertion of this symbol is locale-dependent and occurs even when startRange === endRange [1][4]. Technically, the specification defines this behavior through the following mechanisms: - Collapsing: If the start and end values resolve to the same formatted string, the range is condensed into a single representation [1][2]. - Approximately Sign: The approximately equals symbol (often ~) is added to signify that the value represents a range that has collapsed into a single point due to formatting constraints [5]. - formatRangeToParts: The corresponding formatRangeToParts method exposes this state explicitly, returning an array of parts where the first token may be an "approximatelySign" (type: "approximatelySign") and all other tokens are marked with source: "shared" [4]. This behavior was introduced as part of the Intl.NumberFormat V3 proposal to provide parity with Intl.DateTimeFormat.prototype.formatRange [5][6].

Citations:


Keep equal endpoints on the approximate path. formatRange(3, 3) should still emit the approximate form, and formatRangeToParts(3, 3) should include approximatelySign; the early returns here skip both.

🤖 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 1723 - 1728, The
early equality collapse in range_endpoints_equal and the related fast path in
the range formatting flow are incorrectly bypassing the approximate form for
equal endpoints. Update the range handling in number_format.rs so formatRange
and formatRangeToParts still go through the approximate rendering path when the
two endpoints are equal, ensuring formatRangeToParts includes approximatelySign
instead of returning a single collapsed value.

@proggeramlug proggeramlug merged commit 10fdb31 into main Jun 28, 2026
15 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-5581-numberformat branch June 28, 2026 19:00
proggeramlug added a commit that referenced this pull request Jun 28, 2026
…5773)

Follow-up to #5771 (CodeRabbit). ECMA-402 PartitionNumberRangePattern
renders the approximate ("~") form whenever the two endpoints format to the
same string — including mathematically equal endpoints. The merged code
special-cased equal endpoints to a plain single value, so
`formatRange(3, 3)` returned "3" and `formatRangeToParts(3, 3)` omitted the
`approximatelySign`, diverging from the spec and from Node:

  new Intl.NumberFormat("en-US").formatRange(3, 3)        // "~3"
  new Intl.NumberFormat("en-US").formatRangeToParts(3, 3) // [{approximatelySign,~,shared},{integer,3,shared}]

Drop the `range_endpoints_equal` fast path so equal endpoints flow through
the existing `sx == sy` approximate branch. Verified byte-for-byte against
node v26 and the pinned test262 corpus (intl402/NumberFormat formatRange
/formatRangeToParts: 16 pass / 3 fail unchanged, 0 compile failures).

Co-authored-by: Ralph <ralph@skelpo.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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