diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index abf76367b8..1e925386f3 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -23,6 +23,8 @@ mod locale; mod locales; use locales::{get_canonical_locales_thunk, supported_values_of_thunk}; mod date_collator; +mod install; +use install::install_constructor; mod list_relative_plural; mod number_format; mod number_format_options; @@ -62,12 +64,12 @@ pub(crate) use number_format::{ intl_object_from_value, nf_coerce_number, nf_load, nf_resolved_default, number_format_bound_format_thunk, number_format_bound_resolved_options_thunk, number_format_bound_to_parts_thunk, number_format_format_getter_thunk, - number_format_format_object, number_format_resolved_options_object, - number_format_resolved_options_thunk, number_format_to_parts_thunk, number_instance_parts, - number_parts_from_resolved, parts_to_js_array, push_grouped_integer, push_sign, - push_style_suffix, round_integer_to_place, round_mode_code, round_to_fraction, - round_to_significant, rounding_up, set_round_ctx, significant_count, strip_leading_zeros, - this_intl_object, trim_fraction, NfResolved, + number_format_format_object, number_format_range_thunk, number_format_range_to_parts_thunk, + number_format_resolved_options_object, number_format_resolved_options_thunk, + number_format_to_parts_thunk, number_instance_parts, number_parts_from_resolved, + parts_to_js_array, push_grouped_integer, push_sign, push_style_suffix, round_integer_to_place, + round_mode_code, round_to_fraction, round_to_significant, rounding_up, set_round_ctx, + significant_count, strip_leading_zeros, this_intl_object, trim_fraction, NfResolved, }; pub(crate) use number_format_options::{ configure_number_format, is_well_formed_currency_code, is_well_formed_unit_identifier, @@ -1063,6 +1065,29 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option number_format_bound_to_parts_thunk as *const u8, 1, ); + // `formatRange`/`formatRangeToParts` are installed as own instance + // properties (native Intl method dispatch resolves from own props, + // not the static prototype) but with a *this-based* closure rather + // than a bound one: a detached `nf.formatRange` reference therefore + // loses `this` and the `this_intl_object` guard throws a TypeError + // (formatRange/invoked-as-func.js), matching the non-bound prototype + // method these shadow. + install_function( + obj, + "formatRange", + number_format_range_thunk as *const u8, + 2, + 2, + false, + ); + install_function( + obj, + "formatRangeToParts", + number_format_range_to_parts_thunk as *const u8, + 2, + 2, + false, + ); install_bound_instance_function( obj, "resolvedOptions", @@ -1710,96 +1735,6 @@ fn set_proto_to_string_tag(proto: *mut ObjectHeader, tag: &str) { ); } -fn install_constructor( - ns_obj: *mut ObjectHeader, - name: &str, - ctor_ptr: *const u8, - ctor_length: u32, - methods: &[(&str, *const u8, u32)], - getters: &[(&str, *const u8)], -) { - let ctor = crate::closure::js_closure_alloc(ctor_ptr, 0); - if ctor.is_null() { - return; - } - crate::closure::js_register_closure_rest(ctor_ptr, 0); - crate::object::set_bound_native_closure_name(ctor, name); - crate::object::set_builtin_closure_length(ctor as usize, ctor_length); - crate::object::set_builtin_property_attrs( - ctor as usize, - "name".to_string(), - PropertyAttrs::new(false, false, true), - ); - crate::object::set_builtin_property_attrs( - ctor as usize, - "length".to_string(), - PropertyAttrs::new(false, false, true), - ); - - let ctor_value = js_nanbox_pointer(ctor as i64); - // Generous inline capacity so installing methods plus an accessor getter and - // the toStringTag symbol never bumps `field_count` past the physical slot - // count (which would expose an overflow slot — keys_array.rs #4099). - let proto = js_object_alloc(0, 16); - set_field(proto, "constructor", ctor_value); - set_builtin_attrs(proto, "constructor", PropertyAttrs::new(true, false, true)); - for (method, ptr, arity) in methods.iter().copied() { - install_function(proto, method, ptr, arity, arity, false); - } - // Accessor properties (e.g. `get Intl.NumberFormat.prototype.format`): a - // getter-only descriptor on the prototype so reflection - // (`Object.getOwnPropertyDescriptor(proto, key).get`) sees a function whose - // name is `"get "` and length 0. Instances still carry an own bound - // method for the hot dispatch path (native objects resolve from own props). - for (getter_name, ptr) in getters.iter().copied() { - let closure = crate::closure::js_closure_alloc(ptr, 0); - if closure.is_null() { - continue; - } - crate::closure::js_register_closure_arity(ptr, 0); - crate::object::set_bound_native_closure_name(closure, &format!("get {getter_name}")); - crate::object::set_builtin_closure_length(closure as usize, 0); - crate::object::set_builtin_property_attrs( - closure as usize, - "name".to_string(), - PropertyAttrs::new(false, false, true), - ); - crate::object::set_builtin_property_attrs( - closure as usize, - "length".to_string(), - PropertyAttrs::new(false, false, true), - ); - let getter_bits = js_nanbox_pointer(closure as i64).to_bits(); - unsafe { - crate::object::install_builtin_getter(proto, getter_name, getter_bits); - } - } - set_proto_to_string_tag(proto, &format!("Intl.{name}")); - let proto_value = js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(ctor as usize, "prototype", proto_value); - crate::object::set_builtin_property_attrs( - ctor as usize, - "prototype".to_string(), - PropertyAttrs::new(false, false, false), - ); - - // `supportedLocalesOf(locales, options)` — `.length` is 1, but it reads a - // second `options` argument, so register it rest-style (all args collected) - // and pull both positionally. - let supported = install_function( - ctor as *mut ObjectHeader, - "supportedLocalesOf", - supported_locales_of_thunk as *const u8, - 0, - 1, - true, - ); - crate::closure::closure_set_dynamic_prop(ctor as usize, "supportedLocalesOf", supported); - - set_field(ns_obj, name, ctor_value); - set_builtin_attrs(ns_obj, name, PropertyAttrs::new(true, false, true)); -} - pub fn install_intl_namespace(ns_obj: *mut ObjectHeader) { if ns_obj.is_null() { return; @@ -1834,6 +1769,16 @@ pub fn install_intl_namespace(ns_obj: *mut ObjectHeader) { number_format_to_parts_thunk as *const u8, 1, ), + // `formatRange`/`formatRangeToParts` are plain (non-bound) prototype + // methods (Intl.NumberFormat-v3): a detached reference loses `this` + // and the `this_intl_object` guard throws, so they are installed on + // the prototype only — never as own bound instance functions. + ("formatRange", number_format_range_thunk as *const u8, 2), + ( + "formatRangeToParts", + number_format_range_to_parts_thunk as *const u8, + 2, + ), ( "resolvedOptions", number_format_resolved_options_thunk as *const u8, diff --git a/crates/perry-runtime/src/intl/install.rs b/crates/perry-runtime/src/intl/install.rs new file mode 100644 index 0000000000..4366e4ba42 --- /dev/null +++ b/crates/perry-runtime/src/intl/install.rs @@ -0,0 +1,106 @@ +//! Shared `Intl.*` constructor/prototype installation. +//! +//! `install_constructor` builds the constructor closure, its prototype (plain +//! methods, accessor getters, `Symbol.toStringTag`), the static +//! `supportedLocalesOf`, and registers the pair on the `Intl` namespace. Split +//! out of `intl.rs` to keep that namespace module under the file-size gate; the +//! private helpers it leans on (`set_field`, `install_function`, …) stay in the +//! parent and are reachable here as a descendant module. + +use crate::object::{js_object_alloc, ObjectHeader, PropertyAttrs}; +use crate::value::js_nanbox_pointer; + +use super::{ + install_function, set_builtin_attrs, set_field, set_proto_to_string_tag, + supported_locales_of_thunk, +}; + +pub(super) fn install_constructor( + ns_obj: *mut ObjectHeader, + name: &str, + ctor_ptr: *const u8, + ctor_length: u32, + methods: &[(&str, *const u8, u32)], + getters: &[(&str, *const u8)], +) { + let ctor = crate::closure::js_closure_alloc(ctor_ptr, 0); + if ctor.is_null() { + return; + } + crate::closure::js_register_closure_rest(ctor_ptr, 0); + crate::object::set_bound_native_closure_name(ctor, name); + crate::object::set_builtin_closure_length(ctor as usize, ctor_length); + crate::object::set_builtin_property_attrs( + ctor as usize, + "name".to_string(), + PropertyAttrs::new(false, false, true), + ); + crate::object::set_builtin_property_attrs( + ctor as usize, + "length".to_string(), + PropertyAttrs::new(false, false, true), + ); + + let ctor_value = js_nanbox_pointer(ctor as i64); + // Generous inline capacity so installing methods plus an accessor getter and + // the toStringTag symbol never bumps `field_count` past the physical slot + // count (which would expose an overflow slot — keys_array.rs #4099). + let proto = js_object_alloc(0, 16); + set_field(proto, "constructor", ctor_value); + set_builtin_attrs(proto, "constructor", PropertyAttrs::new(true, false, true)); + for (method, ptr, arity) in methods.iter().copied() { + install_function(proto, method, ptr, arity, arity, false); + } + // Accessor properties (e.g. `get Intl.NumberFormat.prototype.format`): a + // getter-only descriptor on the prototype so reflection + // (`Object.getOwnPropertyDescriptor(proto, key).get`) sees a function whose + // name is `"get "` and length 0. Instances still carry an own bound + // method for the hot dispatch path (native objects resolve from own props). + for (getter_name, ptr) in getters.iter().copied() { + let closure = crate::closure::js_closure_alloc(ptr, 0); + if closure.is_null() { + continue; + } + crate::closure::js_register_closure_arity(ptr, 0); + crate::object::set_bound_native_closure_name(closure, &format!("get {getter_name}")); + crate::object::set_builtin_closure_length(closure as usize, 0); + crate::object::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + PropertyAttrs::new(false, false, true), + ); + crate::object::set_builtin_property_attrs( + closure as usize, + "length".to_string(), + PropertyAttrs::new(false, false, true), + ); + let getter_bits = js_nanbox_pointer(closure as i64).to_bits(); + unsafe { + crate::object::install_builtin_getter(proto, getter_name, getter_bits); + } + } + set_proto_to_string_tag(proto, &format!("Intl.{name}")); + let proto_value = js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(ctor as usize, "prototype", proto_value); + crate::object::set_builtin_property_attrs( + ctor as usize, + "prototype".to_string(), + PropertyAttrs::new(false, false, false), + ); + + // `supportedLocalesOf(locales, options)` — `.length` is 1, but it reads a + // second `options` argument, so register it rest-style (all args collected) + // and pull both positionally. + let supported = install_function( + ctor as *mut ObjectHeader, + "supportedLocalesOf", + supported_locales_of_thunk as *const u8, + 0, + 1, + true, + ); + crate::closure::closure_set_dynamic_prop(ctor as usize, "supportedLocalesOf", supported); + + set_field(ns_obj, name, ctor_value); + set_builtin_attrs(ns_obj, name, PropertyAttrs::new(true, false, true)); +} diff --git a/crates/perry-runtime/src/intl/number_format.rs b/crates/perry-runtime/src/intl/number_format.rs index 414d9377e0..d18c0575b5 100644 --- a/crates/perry-runtime/src/intl/number_format.rs +++ b/crates/perry-runtime/src/intl/number_format.rs @@ -1696,6 +1696,122 @@ pub(crate) extern "C" fn number_format_bound_to_parts_thunk( parts_to_js_array(&number_instance_parts(obj, number)) } +/// Coerce a `formatRange`/`formatRangeToParts` endpoint to its `f64` +/// mathematical value and run the two endpoint checks ECMA-402 places before +/// rendering: `undefined` is a TypeError (the explicit `start`/`end` undefined +/// guard), and a value that coerces to NaN is a RangeError +/// (PartitionNumberRangePattern step 1). Every other value — BigInt, numeric +/// String, ±Infinity, ±0 — coerces and is formatted. Returns the clipped pair. +fn number_range_endpoints(method: &str, start: f64, end: f64) -> (f64, f64) { + let sj = JSValue::from_bits(start.to_bits()); + let ej = JSValue::from_bits(end.to_bits()); + if sj.is_undefined() || ej.is_undefined() { + throw_type_error(&format!( + "Intl.NumberFormat.prototype.{method} called with undefined start or end" + )); + } + let x = nf_coerce_number(start); + let y = nf_coerce_number(end); + if x.is_nan() || y.is_nan() { + throw_range_error(&format!( + "Intl.NumberFormat.prototype.{method} called with a NaN argument" + )); + } + (x, y) +} + +/// 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 +} + +/// `Intl.NumberFormat.prototype.formatRange` — a best-effort +/// PartitionNumberRangePattern: render both endpoints with the instance's +/// formatter, collapse to a single value when the endpoints are mathematically +/// equal, mark the result approximate (`~`) when distinct endpoints round to the +/// same string, and otherwise join the two renderings with an en dash. The exact +/// ICU field-collapsing / locale range pattern is not reproduced. +pub(crate) fn number_format_range_value( + obj: *const ObjectHeader, + method: &str, + start: f64, + end: f64, +) -> f64 { + let (x, y) = number_range_endpoints(method, start, end); + let r = nf_load(obj); + let sx: String = number_parts_from_resolved(&r, x) + .iter() + .map(|(_, v)| v.as_str()) + .collect(); + if range_endpoints_equal(x, y) { + return string_value(&sx); + } + let sy: String = number_parts_from_resolved(&r, y) + .iter() + .map(|(_, v)| v.as_str()) + .collect(); + if sx == sy { + string_value(&format!("~{sx}")) + } else { + string_value(&format!("{sx}\u{2013}{sy}")) + } +} + +/// `Intl.NumberFormat.prototype.formatRangeToParts` — the parts shape of +/// [`number_format_range_value`]. Each segment carries a `source` +/// (`"startRange"`/`"endRange"`/`"shared"`); a collapsed range tags every +/// segment `"shared"`, and the approximate form prepends an `approximatelySign`. +pub(crate) fn number_format_range_parts_value( + obj: *const ObjectHeader, + method: &str, + start: f64, + end: f64, +) -> f64 { + let (x, y) = number_range_endpoints(method, start, end); + let r = nf_load(obj); + let tag = |parts: Vec<(&'static str, String)>, source: &'static str| { + parts.into_iter().map(move |(t, v)| (t, v, source)) + }; + let x_parts = number_parts_from_resolved(&r, x); + if range_endpoints_equal(x, y) { + let shared: Vec<_> = tag(x_parts, "shared").collect(); + return super::date_collator::range_parts_to_js_array(&shared); + } + let y_parts = number_parts_from_resolved(&r, y); + let sx: String = x_parts.iter().map(|(_, v)| v.as_str()).collect(); + let sy: String = y_parts.iter().map(|(_, v)| v.as_str()).collect(); + if sx == sy { + let mut parts: Vec<(&'static str, String, &'static str)> = + vec![("approximatelySign", "~".to_string(), "shared")]; + parts.extend(tag(x_parts, "shared")); + return super::date_collator::range_parts_to_js_array(&parts); + } + let mut parts: Vec<(&'static str, String, &'static str)> = tag(x_parts, "startRange").collect(); + parts.push(("literal", "\u{2013}".to_string(), "shared")); + parts.extend(tag(y_parts, "endRange")); + super::date_collator::range_parts_to_js_array(&parts) +} + +pub(crate) extern "C" fn number_format_range_thunk( + _closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let obj = this_intl_object("formatRange", KIND_NUMBER); + number_format_range_value(obj, "formatRange", start, end) +} + +pub(crate) extern "C" fn number_format_range_to_parts_thunk( + _closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let obj = this_intl_object("formatRangeToParts", KIND_NUMBER); + number_format_range_parts_value(obj, "formatRangeToParts", start, end) +} + pub(crate) fn number_format_resolved_options_object(obj: *const ObjectHeader) -> f64 { let r = nf_load(obj); let out = js_object_alloc(0, 16);