From 686591b65dc8dcf0a929a46e338990b035a287bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 4 Jul 2026 09:33:44 +0200 Subject: [PATCH 1/3] =?UTF-8?q?fix(intl):=20#5896=20=E2=80=94=20class=20X?= =?UTF-8?q?=20extends=20Intl.=20super()=20+=20Locale=20in=20Canonica?= =?UTF-8?q?lizeLocaleList?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Intl service constructor (ListFormat/Locale/PluralRules/RelativeTimeFormat/ Segmenter/DisplayNames/…) allocates and returns a fresh branded object and throws "Constructor Intl.X requires 'new'" when new.target is undefined. A `class X extends Intl.` routes its super() through the generic runtime-value dispatcher (js_fetch_or_value_super / js_super_construct_apply), which invoked the parent constructor with no new.target — so every Intl subclass threw at construction. Mirror the existing Temporal-subclass path: recognize an Intl constructor parent (by its closure func_ptr), run it with new.target set to the parent, and re-home the returned instance's own fields (the __intl* brand + bound format/resolvedOptions methods) onto the subclass `this` via js_object_copy_own_fields. Added is_intl_constructor_value + intl_subclass_super in intl.rs, wired into both super() lowerings. Also fix CanonicalizeLocaleList for an Intl.Locale (or Locale-subclass) argument: per spec step 2 a value with an [[InitializedLocale]] slot is the single-element list « locale », read from its [[Locale]] slot directly — never iterated as an array-like nor run through the (user-overridable) toString. get_canonical_locales treated a Locale as a length-0 array-like → empty result; now reads the __localeFull brand field. Added locale_instance_tag helper. Fixes test262: intl402/ListFormat/constructor/constructor/subclassing, intl402/Locale/subclassing, intl402/Locale/canonicalize-locale-list-take-locale, intl402/PluralRules/can-be-subclassed, intl402/RelativeTimeFormat/constructor/constructor/subclassing. Zero regressions across the intl402 constructor slice. --- crates/perry-runtime/src/intl.rs | 101 ++++++++++++++++++ crates/perry-runtime/src/intl/locale.rs | 2 +- crates/perry-runtime/src/intl/locales.rs | 19 +++- .../src/object/class_constructors.rs | 20 ++++ .../src/object/global_this/fetch_globals.rs | 19 ++++ 5 files changed, 158 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index b212b230fd..20a30a0f63 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -571,10 +571,29 @@ fn js_has_index(obj: f64, index: u32) -> bool { /// an Object (an `Intl.Locale` or anything ToString-able), else `TypeError`; the /// resulting tag is canonicalized (`RangeError` if structurally invalid) and /// pushed if not already present. +/// If `value` is an `Intl.Locale` instance (its `[[InitializedLocale]]` slot, +/// modeled by the `__intlKind == "Locale"` internal field) return its +/// `[[Locale]]` tag string — the canonical `__localeFull` field. Per +/// CanonicalizeLocaleList, a Locale element contributes `.toString()`'s value +/// *without invoking the (user-overridable) `toString` method*: the abstract op +/// reads the internal slot directly. Also matches `class X extends Intl.Locale` +/// subclass instances, which carry the copied brand fields (see +/// `intl_subclass_super`). +pub(super) fn locale_instance_tag(value: f64) -> Option { + let obj = object_ptr_from_value(value)?; + if get_string_field(obj, KEY_KIND).as_deref() != Some("Locale") { + return None; + } + // `__localeFull` — the constructor-canonicalized full tag. + get_string_field(obj, "__localeFull") +} + fn push_locale_element(out: &mut Vec, value: f64) { let jv = JSValue::from_bits(value.to_bits()); let tag = if jv.is_any_string() { string_from_string_value(value).unwrap_or_default() + } else if let Some(locale_tag) = locale_instance_tag(value) { + locale_tag } else if object_ptr_from_value(value).is_some() { value_to_string(value) } else { @@ -607,6 +626,17 @@ fn locales_from_value(locales: f64) -> Vec { }; return vec![canonical]; } + // CanonicalizeLocaleList step 2: a value with an `[[InitializedLocale]]` + // slot (an `Intl.Locale`, or a `class X extends Intl.Locale` subclass + // instance) is wrapped as the single-element list « locale » — its + // `[[Locale]]` slot is read directly, NOT iterated as an array-like nor run + // through `toString`. + if let Some(tag) = locale_instance_tag(locales) { + let Some(canonical) = canonicalize_language_tag(&tag) else { + throw_invalid_language_tag(&tag); + }; + return vec![canonical]; + } if let Some(arr) = array_ptr_from_value(locales) { let len = js_array_length(arr); let mut out = Vec::with_capacity(len as usize); @@ -1699,6 +1729,77 @@ extern "C" fn plural_rules_constructor_thunk(closure: *const ClosureHeader, rest ) } +/// The compiled function pointers of every `Intl.*` service constructor thunk. +/// Used by [`intl_subclass_super`] to recognize a `class X extends Intl.` +/// parent value from its closure so `super(...)` can construct it correctly +/// (with `new.target` set) rather than tripping the `require_new_target` guard. +fn intl_constructor_func_ptrs() -> [*const u8; 10] { + [ + number_format_constructor_thunk as *const u8, + date_time_format_constructor_thunk as *const u8, + collator_constructor_thunk as *const u8, + segmenter_constructor_thunk as *const u8, + list_format_constructor_thunk as *const u8, + relative_time_format_constructor_thunk as *const u8, + plural_rules_constructor_thunk as *const u8, + duration_format::constructor_thunk as *const u8, + display_names::constructor_thunk as *const u8, + locale::locale_constructor_thunk as *const u8, + ] +} + +/// `true` when `parent_val` is (the closure for) an `Intl.*` service +/// constructor. `class X extends Intl.ListFormat` routes its `super()` through +/// the generic runtime-value dispatcher, which would invoke the constructor +/// without a `new.target` and throw "Constructor Intl.X requires 'new'"; this +/// lets the super-call path recognize the parent and construct it properly. +pub(crate) fn is_intl_constructor_value(parent_val: f64) -> bool { + let jsval = JSValue::from_bits(parent_val.to_bits()); + if !jsval.is_pointer() { + return false; + } + let closure = jsval.as_pointer() as *const ClosureHeader; + if closure.is_null() { + return false; + } + let fp = unsafe { (*closure).func_ptr }; + intl_constructor_func_ptrs().iter().any(|p| *p == fp) +} + +/// `class X extends Intl.` super-call handling. An `Intl.*` service +/// constructor allocates and returns a fresh branded object (internal +/// `__intl*` fields plus own `format`/`resolvedOptions`/… methods) and does not +/// mutate the implicit `this`; it also throws "requires 'new'" when +/// `new.target` is undefined. So when `parent_val` is an Intl constructor: set +/// `new.target` to the parent for the duration of the construct (so the guard +/// passes), run it, then copy every own field of the returned instance onto the +/// subclass `this` — giving `this` the Intl brand and its bound methods. +/// Returns `true` when handled (mirrors [`temporal_subclass_super`]). +pub(crate) unsafe fn intl_subclass_super( + parent_val: f64, + this_box: f64, + args_ptr: *const f64, + args_len: usize, +) -> bool { + if !is_intl_constructor_value(parent_val) { + return false; + } + let prev_this = crate::object::js_implicit_this_set(this_box); + let prev_nt = crate::object::js_new_target_set(parent_val); + let instance = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); + crate::object::js_new_target_set(prev_nt); + crate::object::js_implicit_this_set(prev_this); + // Re-home the freshly-built instance's brand + bound methods onto `this`. + let this_bits = this_box.to_bits(); + if (this_bits >> 48) == 0x7FFD { + let dst = (this_bits & 0x0000_FFFF_FFFF_FFFF) as i64; + if dst >= 0x10000 { + crate::object::js_object_copy_own_fields(dst, instance); + } + } + true +} + fn supported_locales_array(locales: f64, options: f64) -> f64 { // `supportedLocalesOf(locales, options)`: // 1. requestedLocales = ? CanonicalizeLocaleList(locales) ← runs FIRST, diff --git a/crates/perry-runtime/src/intl/locale.rs b/crates/perry-runtime/src/intl/locale.rs index 839144ebbf..a49e8189ec 100644 --- a/crates/perry-runtime/src/intl/locale.rs +++ b/crates/perry-runtime/src/intl/locale.rs @@ -649,7 +649,7 @@ fn transform_instance(obj: *const ObjectHeader, transform: fn(&mut ParsedLocale) make_locale_instance(proto, &p) } -extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { super::require_new_target("Locale"); let tag_value = super::rest_arg(rest, 0); let options_value = super::rest_arg(rest, 1); diff --git a/crates/perry-runtime/src/intl/locales.rs b/crates/perry-runtime/src/intl/locales.rs index 813c3169b2..69a4515f29 100644 --- a/crates/perry-runtime/src/intl/locales.rs +++ b/crates/perry-runtime/src/intl/locales.rs @@ -5,8 +5,8 @@ use super::{ array_ptr_from_value, canonicalize_language_tag, get_field, get_number_field, - object_ptr_from_value, string_from_string_value, string_value, throw_invalid_language_tag, - throw_range_error, throw_type_error, value_to_string, + locale_instance_tag, object_ptr_from_value, string_from_string_value, string_value, + throw_invalid_language_tag, throw_range_error, throw_type_error, value_to_string, }; use crate::array::{js_array_alloc, js_array_get_f64, js_array_length, js_array_push_f64}; use crate::closure::ClosureHeader; @@ -20,6 +20,13 @@ fn locale_list_element_tag(value: f64) -> String { if js.is_any_string() { return string_from_string_value(value).unwrap_or_default(); } + // An `Intl.Locale` (or `class X extends Intl.Locale` subclass) element: + // CanonicalizeLocaleList reads its `[[Locale]]` slot directly, WITHOUT + // calling the (user-overridable) `toString` — checked before the generic + // ToString path below (test262 canonicalize-locale-list-take-locale.js). + if let Some(tag) = locale_instance_tag(value) { + return tag; + } // Object (but not a Symbol, which is pointer-shaped yet a primitive). if js.is_pointer() && unsafe { crate::symbol::js_is_symbol(value) } == 0 { return value_to_string(value); @@ -64,6 +71,14 @@ fn get_canonical_locales(locales: f64) -> f64 { push_canonical_locale(&mut seen, &tag); return canonical_locales_array(&seen); } + // CanonicalizeLocaleList step 2: a value with an `[[InitializedLocale]]` + // slot (an `Intl.Locale` or a subclass instance) is the single-element list + // « locale », read from its `[[Locale]]` slot — never iterated as an + // array-like nor stringified via `toString`. + if let Some(tag) = locale_instance_tag(locales) { + push_canonical_locale(&mut seen, &tag); + return canonical_locales_array(&seen); + } if let Some(arr) = array_ptr_from_value(locales) { let len = js_array_length(arr); for i in 0..len { diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 6b7f1d2e79..d5fafde37d 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -408,6 +408,26 @@ pub unsafe extern "C" fn js_super_construct_apply( ); } } + // `class X extends Intl.` via `super(...spread)`: the decl-time parent + // value is the Intl constructor closure; run it (new.target set) and re-home + // the branded instance onto `this`, the spread counterpart of the + // `js_fetch_or_value_super` Intl branch. + { + let parent_val = crate::object::class_registry::js_get_dynamic_parent_value(child_cid); + if crate::intl::is_intl_constructor_value(parent_val) { + let this_box = crate::value::js_nanbox_pointer(this_raw); + let n = if arr.is_null() { + 0 + } else { + crate::array::js_array_length(arr) + } as usize; + let mut flat: Vec = Vec::with_capacity(n); + for i in 0..n { + flat.push(crate::array::js_array_get_f64(arr, i as u32)); + } + crate::intl::intl_subclass_super(parent_val, this_box, flat.as_ptr(), flat.len()); + } + } undef } diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs index c5be1ae07f..1e2f826f9b 100644 --- a/crates/perry-runtime/src/object/global_this/fetch_globals.rs +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -550,6 +550,25 @@ pub unsafe extern "C" fn js_fetch_or_value_super( return undef; } } + // `class X extends Intl.` (`super(locales, options)`): an Intl service + // constructor returns a fresh branded object and throws "requires 'new'" + // when `new.target` is undefined, so — like Temporal — recognize the parent + // and construct it with `new.target` set, re-homing the instance's brand + + // methods onto `this`. `parent_val` can arrive stale for an aliased heritage + // (`const L = Intl.Locale; class X extends L`); recover the decl-time parent. + { + let intl_parent = if crate::intl::is_intl_constructor_value(parent_val) { + parent_val + } else if let Some(obj) = subclass_this_object_ptr(this_box) { + let cid = crate::object::js_object_get_class_id(obj); + crate::object::class_registry::js_get_dynamic_parent_value(cid) + } else { + parent_val + }; + if crate::intl::intl_subclass_super(intl_parent, this_box, args_ptr, args_len) { + return undef; + } + } // Resolve the parent constructor kind from the value first. When the // `extends` expression is an alias of `global.Request`/`global.Response` // (`@hono/node-server`'s `class Request extends GlobalRequest`), the alias From 51fd9638250f6cf9d9a6f9f7d110b3441a6535f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 4 Jul 2026 09:50:49 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(intl):=20#5896=20=E2=80=94=20value=20in?= =?UTF-8?q?stanceof=20Intl.=20via=20prototype-chain=20walk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intl service instances are plain heap objects whose [[Prototype]] is set to Intl..prototype (object_set_static_prototype), but the dynamic-instanceof path has no class-id for them and no generic prototype walk, so `inst instanceof Intl.DisplayNames` returned false even though Object.getPrototypeOf(inst) === Intl.DisplayNames.prototype. Add intl_instanceof: when the RHS is an Intl constructor, walk the value's static-prototype chain and compare each link against the constructor's .prototype. Also resolves the subclass case (the subclass this reaches Intl..prototype through its own prototype). Fixes test262 intl402/DisplayNames/options-{type,style,fallback,languagedisplay, localeMatcher}-valid + options-random-properties-unchecked, and Segmenter/constructor/constructor/options-localeMatcher-valid — 7 more, zero regressions (12 total on this branch across the intl402 constructor slice). --- crates/perry-runtime/src/intl.rs | 48 +++++++++++++++++++ crates/perry-runtime/src/object/instanceof.rs | 10 ++++ 2 files changed, 58 insertions(+) diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index 20a30a0f63..b06556932a 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -1800,6 +1800,54 @@ pub(crate) unsafe fn intl_subclass_super( true } +/// `value instanceof Intl.` (OrdinaryHasInstance) when the right operand +/// is an Intl service constructor. Intl instances are plain heap objects whose +/// `[[Prototype]]` is set to `Intl..prototype` (via +/// `object_set_static_prototype`), but the generic dynamic-`instanceof` path has +/// no class-id for them and no generic prototype walk, so it returned `false` +/// even though `Object.getPrototypeOf(inst) === Intl..prototype`. Walk the +/// value's static-prototype chain and compare each link against the +/// constructor's `.prototype`. Returns `None` when `type_ref` is not an Intl +/// constructor (caller keeps its existing resolution); `Some(bool)` otherwise. +pub(crate) fn intl_instanceof(value: f64, type_ref: f64) -> Option { + if !is_intl_constructor_value(type_ref) { + return None; + } + let jsval = JSValue::from_bits(type_ref.to_bits()); + let closure = jsval.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(closure, "prototype"); + let proto_js = JSValue::from_bits(proto.to_bits()); + if !proto_js.is_pointer() { + return Some(false); + } + let target_bits = proto.to_bits(); + // Walk `value`'s [[Prototype]] chain (bounded against cycles). + let mut cur = value.to_bits(); + for _ in 0..64 { + let top16 = cur >> 48; + let raw = if top16 == 0x7FFD { + (cur & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top16 == 0 { + cur as usize + } else { + return Some(false); + }; + if raw < 0x10000 { + return Some(false); + } + match crate::object::prototype_chain::object_static_prototype(raw) { + Some(p) => { + if p == target_bits { + return Some(true); + } + cur = p; + } + None => return Some(false), + } + } + Some(false) +} + fn supported_locales_array(locales: f64, options: f64) -> f64 { // `supportedLocalesOf(locales, options)`: // 1. requestedLocales = ? CanonicalizeLocaleList(locales) ← runs FIRST, diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 03b5efce29..8f400e23a7 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -353,6 +353,16 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { f64::from_bits(TAG_FALSE) }; } + // `inst instanceof Intl.`: Intl instances are plain heap objects whose + // `[[Prototype]]` is `Intl..prototype` but carry no class-id, so the + // arms above can't match them. Walk their static-prototype chain. + if let Some(is_inst) = crate::intl::intl_instanceof(value, type_ref) { + return if is_inst { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + }; + } js_instanceof_dynamic_tail(value, type_ref) } From 3a39e2d13f9353b1e31e417719c529f6058afc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 4 Jul 2026 11:06:28 +0200 Subject: [PATCH 3/3] refactor(intl): split subclass/instanceof/locale-list helpers into intl/subclass.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subclass + instanceof + CanonicalizeLocaleList helpers pushed intl.rs past the 2,000-line file-size gate. Move is_intl_constructor_value / intl_subclass_super / intl_instanceof / locale_instance_tag / push_locale_element into a new intl/subclass.rs sibling module (re-exported from intl.rs); make the seven in-file Intl constructor thunks pub(super) so the recognition helper can reference them. Pure code motion — no behavior change (intl402 slice still +12, zero regressions). intl.rs back under the gate (1998 lines). --- crates/perry-runtime/src/intl.rs | 201 ++++------------------ crates/perry-runtime/src/intl/subclass.rs | 177 +++++++++++++++++++ 2 files changed, 207 insertions(+), 171 deletions(-) create mode 100644 crates/perry-runtime/src/intl/subclass.rs diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index b06556932a..f8c24af2c9 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -27,6 +27,9 @@ use locales::{get_canonical_locales_thunk, supported_values_of_thunk}; mod date_collator; mod install; use install::install_constructor; +mod subclass; +pub(crate) use subclass::{intl_instanceof, intl_subclass_super, is_intl_constructor_value}; +use subclass::{locale_instance_tag, push_locale_element}; mod list_relative_plural; mod number_format; mod number_format_digits; @@ -567,47 +570,6 @@ fn js_has_index(obj: f64, index: u32) -> bool { crate::object::js_object_has_property(obj, key).to_bits() == crate::value::TAG_TRUE } -/// CanonicalizeLocaleList element handler: a present element must be a String or -/// an Object (an `Intl.Locale` or anything ToString-able), else `TypeError`; the -/// resulting tag is canonicalized (`RangeError` if structurally invalid) and -/// pushed if not already present. -/// If `value` is an `Intl.Locale` instance (its `[[InitializedLocale]]` slot, -/// modeled by the `__intlKind == "Locale"` internal field) return its -/// `[[Locale]]` tag string — the canonical `__localeFull` field. Per -/// CanonicalizeLocaleList, a Locale element contributes `.toString()`'s value -/// *without invoking the (user-overridable) `toString` method*: the abstract op -/// reads the internal slot directly. Also matches `class X extends Intl.Locale` -/// subclass instances, which carry the copied brand fields (see -/// `intl_subclass_super`). -pub(super) fn locale_instance_tag(value: f64) -> Option { - let obj = object_ptr_from_value(value)?; - if get_string_field(obj, KEY_KIND).as_deref() != Some("Locale") { - return None; - } - // `__localeFull` — the constructor-canonicalized full tag. - get_string_field(obj, "__localeFull") -} - -fn push_locale_element(out: &mut Vec, value: f64) { - let jv = JSValue::from_bits(value.to_bits()); - let tag = if jv.is_any_string() { - string_from_string_value(value).unwrap_or_default() - } else if let Some(locale_tag) = locale_instance_tag(value) { - locale_tag - } else if object_ptr_from_value(value).is_some() { - value_to_string(value) - } else { - // undefined / null / boolean / number / Symbol element → TypeError. - throw_type_error("locale must be a String or Object"); - }; - let Some(canonical) = canonicalize_language_tag(&tag) else { - throw_invalid_language_tag(&tag); - }; - if !out.iter().any(|existing| existing == &canonical) { - out.push(canonical); - } -} - fn locales_from_value(locales: f64) -> Vec { let js = JSValue::from_bits(locales.to_bits()); // CanonicalizeLocaleList(undefined) is the empty list; `null` fails ToObject @@ -627,10 +589,8 @@ fn locales_from_value(locales: f64) -> Vec { return vec![canonical]; } // CanonicalizeLocaleList step 2: a value with an `[[InitializedLocale]]` - // slot (an `Intl.Locale`, or a `class X extends Intl.Locale` subclass - // instance) is wrapped as the single-element list « locale » — its - // `[[Locale]]` slot is read directly, NOT iterated as an array-like nor run - // through `toString`. + // slot (an `Intl.Locale` / subclass instance) is the single-element list + // « locale », read from its slot — not iterated nor `toString`-ed. if let Some(tag) = locale_instance_tag(locales) { let Some(canonical) = canonicalize_language_tag(&tag) else { throw_invalid_language_tag(&tag); @@ -1669,11 +1629,17 @@ fn install_bound_instance_function( closure } -extern "C" fn number_format_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn number_format_constructor_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { make_instance(closure, KIND_NUMBER, rest_arg(rest, 0), rest_arg(rest, 1)) } -extern "C" fn date_time_format_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn date_time_format_constructor_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { make_instance( closure, KIND_DATE_TIME, @@ -1682,11 +1648,17 @@ extern "C" fn date_time_format_constructor_thunk(closure: *const ClosureHeader, ) } -extern "C" fn collator_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn collator_constructor_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { make_instance(closure, KIND_COLLATOR, rest_arg(rest, 0), rest_arg(rest, 1)) } -extern "C" fn segmenter_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn segmenter_constructor_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { require_new_target("Segmenter"); make_instance( closure, @@ -1696,7 +1668,10 @@ extern "C" fn segmenter_constructor_thunk(closure: *const ClosureHeader, rest: f ) } -extern "C" fn list_format_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn list_format_constructor_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { require_new_target("ListFormat"); make_instance( closure, @@ -1706,7 +1681,7 @@ extern "C" fn list_format_constructor_thunk(closure: *const ClosureHeader, rest: ) } -extern "C" fn relative_time_format_constructor_thunk( +pub(super) extern "C" fn relative_time_format_constructor_thunk( closure: *const ClosureHeader, rest: f64, ) -> f64 { @@ -1719,7 +1694,10 @@ extern "C" fn relative_time_format_constructor_thunk( ) } -extern "C" fn plural_rules_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn plural_rules_constructor_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { require_new_target("PluralRules"); make_instance( closure, @@ -1729,125 +1707,6 @@ extern "C" fn plural_rules_constructor_thunk(closure: *const ClosureHeader, rest ) } -/// The compiled function pointers of every `Intl.*` service constructor thunk. -/// Used by [`intl_subclass_super`] to recognize a `class X extends Intl.` -/// parent value from its closure so `super(...)` can construct it correctly -/// (with `new.target` set) rather than tripping the `require_new_target` guard. -fn intl_constructor_func_ptrs() -> [*const u8; 10] { - [ - number_format_constructor_thunk as *const u8, - date_time_format_constructor_thunk as *const u8, - collator_constructor_thunk as *const u8, - segmenter_constructor_thunk as *const u8, - list_format_constructor_thunk as *const u8, - relative_time_format_constructor_thunk as *const u8, - plural_rules_constructor_thunk as *const u8, - duration_format::constructor_thunk as *const u8, - display_names::constructor_thunk as *const u8, - locale::locale_constructor_thunk as *const u8, - ] -} - -/// `true` when `parent_val` is (the closure for) an `Intl.*` service -/// constructor. `class X extends Intl.ListFormat` routes its `super()` through -/// the generic runtime-value dispatcher, which would invoke the constructor -/// without a `new.target` and throw "Constructor Intl.X requires 'new'"; this -/// lets the super-call path recognize the parent and construct it properly. -pub(crate) fn is_intl_constructor_value(parent_val: f64) -> bool { - let jsval = JSValue::from_bits(parent_val.to_bits()); - if !jsval.is_pointer() { - return false; - } - let closure = jsval.as_pointer() as *const ClosureHeader; - if closure.is_null() { - return false; - } - let fp = unsafe { (*closure).func_ptr }; - intl_constructor_func_ptrs().iter().any(|p| *p == fp) -} - -/// `class X extends Intl.` super-call handling. An `Intl.*` service -/// constructor allocates and returns a fresh branded object (internal -/// `__intl*` fields plus own `format`/`resolvedOptions`/… methods) and does not -/// mutate the implicit `this`; it also throws "requires 'new'" when -/// `new.target` is undefined. So when `parent_val` is an Intl constructor: set -/// `new.target` to the parent for the duration of the construct (so the guard -/// passes), run it, then copy every own field of the returned instance onto the -/// subclass `this` — giving `this` the Intl brand and its bound methods. -/// Returns `true` when handled (mirrors [`temporal_subclass_super`]). -pub(crate) unsafe fn intl_subclass_super( - parent_val: f64, - this_box: f64, - args_ptr: *const f64, - args_len: usize, -) -> bool { - if !is_intl_constructor_value(parent_val) { - return false; - } - let prev_this = crate::object::js_implicit_this_set(this_box); - let prev_nt = crate::object::js_new_target_set(parent_val); - let instance = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); - crate::object::js_new_target_set(prev_nt); - crate::object::js_implicit_this_set(prev_this); - // Re-home the freshly-built instance's brand + bound methods onto `this`. - let this_bits = this_box.to_bits(); - if (this_bits >> 48) == 0x7FFD { - let dst = (this_bits & 0x0000_FFFF_FFFF_FFFF) as i64; - if dst >= 0x10000 { - crate::object::js_object_copy_own_fields(dst, instance); - } - } - true -} - -/// `value instanceof Intl.` (OrdinaryHasInstance) when the right operand -/// is an Intl service constructor. Intl instances are plain heap objects whose -/// `[[Prototype]]` is set to `Intl..prototype` (via -/// `object_set_static_prototype`), but the generic dynamic-`instanceof` path has -/// no class-id for them and no generic prototype walk, so it returned `false` -/// even though `Object.getPrototypeOf(inst) === Intl..prototype`. Walk the -/// value's static-prototype chain and compare each link against the -/// constructor's `.prototype`. Returns `None` when `type_ref` is not an Intl -/// constructor (caller keeps its existing resolution); `Some(bool)` otherwise. -pub(crate) fn intl_instanceof(value: f64, type_ref: f64) -> Option { - if !is_intl_constructor_value(type_ref) { - return None; - } - let jsval = JSValue::from_bits(type_ref.to_bits()); - let closure = jsval.as_pointer::() as usize; - let proto = crate::closure::closure_get_dynamic_prop(closure, "prototype"); - let proto_js = JSValue::from_bits(proto.to_bits()); - if !proto_js.is_pointer() { - return Some(false); - } - let target_bits = proto.to_bits(); - // Walk `value`'s [[Prototype]] chain (bounded against cycles). - let mut cur = value.to_bits(); - for _ in 0..64 { - let top16 = cur >> 48; - let raw = if top16 == 0x7FFD { - (cur & 0x0000_FFFF_FFFF_FFFF) as usize - } else if top16 == 0 { - cur as usize - } else { - return Some(false); - }; - if raw < 0x10000 { - return Some(false); - } - match crate::object::prototype_chain::object_static_prototype(raw) { - Some(p) => { - if p == target_bits { - return Some(true); - } - cur = p; - } - None => return Some(false), - } - } - Some(false) -} - fn supported_locales_array(locales: f64, options: f64) -> f64 { // `supportedLocalesOf(locales, options)`: // 1. requestedLocales = ? CanonicalizeLocaleList(locales) ← runs FIRST, diff --git a/crates/perry-runtime/src/intl/subclass.rs b/crates/perry-runtime/src/intl/subclass.rs new file mode 100644 index 0000000000..ef67543ba0 --- /dev/null +++ b/crates/perry-runtime/src/intl/subclass.rs @@ -0,0 +1,177 @@ +//! `class X extends Intl.` construction + `instanceof Intl.` +//! support. Split out of `intl.rs` to keep that file under the workspace's +//! 2,000-line ceiling. The Intl-constructor recognition helper +//! (`super::is_intl_constructor_value`) stays in `intl.rs` next to the +//! constructor thunks it matches against. + +use super::{ + canonicalize_language_tag, get_string_field, object_ptr_from_value, string_from_string_value, + throw_invalid_language_tag, throw_type_error, value_to_string, KEY_KIND, +}; +use crate::closure::ClosureHeader; +use crate::value::JSValue; + +/// CanonicalizeLocaleList element handler: a present element must be a String or +/// an Object (an `Intl.Locale` or anything ToString-able), else `TypeError`; the +/// resulting tag is canonicalized (`RangeError` if structurally invalid) and +/// pushed if not already present. +pub(super) fn push_locale_element(out: &mut Vec, value: f64) { + let jv = JSValue::from_bits(value.to_bits()); + let tag = if jv.is_any_string() { + string_from_string_value(value).unwrap_or_default() + } else if let Some(locale_tag) = locale_instance_tag(value) { + locale_tag + } else if object_ptr_from_value(value).is_some() { + value_to_string(value) + } else { + // undefined / null / boolean / number / Symbol element → TypeError. + throw_type_error("locale must be a String or Object"); + }; + let Some(canonical) = canonicalize_language_tag(&tag) else { + throw_invalid_language_tag(&tag); + }; + if !out.iter().any(|existing| existing == &canonical) { + out.push(canonical); + } +} + +/// If `value` is an `Intl.Locale` instance (its `[[InitializedLocale]]` slot, +/// modeled by the `__intlKind == "Locale"` internal field) return its +/// `[[Locale]]` tag string — the canonical `__localeFull` field. Per +/// CanonicalizeLocaleList, a Locale element contributes `.toString()`'s value +/// *without invoking the (user-overridable) `toString` method*: the abstract op +/// reads the internal slot directly. Also matches `class X extends Intl.Locale` +/// subclass instances, which carry the copied brand fields (see +/// `intl_subclass_super`). +pub(super) fn locale_instance_tag(value: f64) -> Option { + let obj = object_ptr_from_value(value)?; + if get_string_field(obj, KEY_KIND).as_deref() != Some("Locale") { + return None; + } + // `__localeFull` — the constructor-canonicalized full tag. + get_string_field(obj, "__localeFull") +} + +/// The compiled function pointers of every `Intl.*` service constructor thunk. +/// Used by [`is_intl_constructor_value`] to recognize a `class X extends +/// Intl.` parent value from its closure so `super(...)` can construct it +/// correctly (with `new.target` set) rather than tripping the +/// `require_new_target` guard. +fn intl_constructor_func_ptrs() -> [*const u8; 10] { + [ + super::number_format_constructor_thunk as *const u8, + super::date_time_format_constructor_thunk as *const u8, + super::collator_constructor_thunk as *const u8, + super::segmenter_constructor_thunk as *const u8, + super::list_format_constructor_thunk as *const u8, + super::relative_time_format_constructor_thunk as *const u8, + super::plural_rules_constructor_thunk as *const u8, + super::duration_format::constructor_thunk as *const u8, + super::display_names::constructor_thunk as *const u8, + super::locale::locale_constructor_thunk as *const u8, + ] +} + +/// `true` when `parent_val` is (the closure for) an `Intl.*` service +/// constructor. `class X extends Intl.ListFormat` routes its `super()` through +/// the generic runtime-value dispatcher, which would invoke the constructor +/// without a `new.target` and throw "Constructor Intl.X requires 'new'"; this +/// lets the super-call path recognize the parent and construct it properly. +pub(crate) fn is_intl_constructor_value(parent_val: f64) -> bool { + let jsval = JSValue::from_bits(parent_val.to_bits()); + if !jsval.is_pointer() { + return false; + } + let closure = jsval.as_pointer() as *const ClosureHeader; + if closure.is_null() { + return false; + } + let fp = unsafe { (*closure).func_ptr }; + intl_constructor_func_ptrs().iter().any(|p| *p == fp) +} + +/// `class X extends Intl.` super-call handling. An `Intl.*` service +/// constructor allocates and returns a fresh branded object (internal +/// `__intl*` fields plus own `format`/`resolvedOptions`/… methods) and does not +/// mutate the implicit `this`; it also throws "requires 'new'" when +/// `new.target` is undefined. So when `parent_val` is an Intl constructor: set +/// `new.target` to the parent for the duration of the construct (so the guard +/// passes), run it, then copy every own field of the returned instance onto the +/// subclass `this` — giving `this` the Intl brand and its bound methods. +/// Returns `true` when handled (mirrors `temporal_subclass_super`). +/// +/// # Safety +/// `args_ptr` must point at `args_len` readable f64 slots (or be null when +/// `args_len` is 0). +pub(crate) unsafe fn intl_subclass_super( + parent_val: f64, + this_box: f64, + args_ptr: *const f64, + args_len: usize, +) -> bool { + if !is_intl_constructor_value(parent_val) { + return false; + } + let prev_this = crate::object::js_implicit_this_set(this_box); + let prev_nt = crate::object::js_new_target_set(parent_val); + let instance = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); + crate::object::js_new_target_set(prev_nt); + crate::object::js_implicit_this_set(prev_this); + // Re-home the freshly-built instance's brand + bound methods onto `this`. + let this_bits = this_box.to_bits(); + if (this_bits >> 48) == 0x7FFD { + let dst = (this_bits & 0x0000_FFFF_FFFF_FFFF) as i64; + if dst >= 0x10000 { + crate::object::js_object_copy_own_fields(dst, instance); + } + } + true +} + +/// `value instanceof Intl.` (OrdinaryHasInstance) when the right operand +/// is an Intl service constructor. Intl instances are plain heap objects whose +/// `[[Prototype]]` is set to `Intl..prototype` (via +/// `object_set_static_prototype`), but the generic dynamic-`instanceof` path has +/// no class-id for them and no generic prototype walk, so it returned `false` +/// even though `Object.getPrototypeOf(inst) === Intl..prototype`. Walk the +/// value's static-prototype chain and compare each link against the +/// constructor's `.prototype`. Returns `None` when `type_ref` is not an Intl +/// constructor (caller keeps its existing resolution); `Some(bool)` otherwise. +pub(crate) fn intl_instanceof(value: f64, type_ref: f64) -> Option { + if !is_intl_constructor_value(type_ref) { + return None; + } + let jsval = JSValue::from_bits(type_ref.to_bits()); + let closure = jsval.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(closure, "prototype"); + let proto_js = JSValue::from_bits(proto.to_bits()); + if !proto_js.is_pointer() { + return Some(false); + } + let target_bits = proto.to_bits(); + // Walk `value`'s [[Prototype]] chain (bounded against cycles). + let mut cur = value.to_bits(); + for _ in 0..64 { + let top16 = cur >> 48; + let raw = if top16 == 0x7FFD { + (cur & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top16 == 0 { + cur as usize + } else { + return Some(false); + }; + if raw < 0x10000 { + return Some(false); + } + match crate::object::prototype_chain::object_static_prototype(raw) { + Some(p) => { + if p == target_bits { + return Some(true); + } + cur = p; + } + None => return Some(false), + } + } + Some(false) +}