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
66 changes: 37 additions & 29 deletions crates/perry-runtime/src/intl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -567,28 +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.
fn push_locale_element(out: &mut Vec<String>, 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 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<String> {
let js = JSValue::from_bits(locales.to_bits());
// CanonicalizeLocaleList(undefined) is the empty list; `null` fails ToObject
Expand All @@ -607,6 +588,15 @@ fn locales_from_value(locales: f64) -> Vec<String> {
};
return vec![canonical];
}
// CanonicalizeLocaleList step 2: a value with an `[[InitializedLocale]]`
// 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);
};
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);
Expand Down Expand Up @@ -1639,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,
Expand All @@ -1652,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,
Expand All @@ -1666,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,
Expand All @@ -1676,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 {
Expand All @@ -1689,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,
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/intl/locale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 17 additions & 2 deletions crates/perry-runtime/src/intl/locales.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
177 changes: 177 additions & 0 deletions crates/perry-runtime/src/intl/subclass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//! `class X extends Intl.<Ctor>` construction + `instanceof Intl.<Ctor>`
//! 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<String>, 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<String> {
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.<Ctor>` 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.<Ctor>` 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.<Ctor>` (OrdinaryHasInstance) when the right operand
/// is an Intl service constructor. Intl instances are plain heap objects whose
/// `[[Prototype]]` is set to `Intl.<Ctor>.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.<Ctor>.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<bool> {
if !is_intl_constructor_value(type_ref) {
return None;
}
let jsval = JSValue::from_bits(type_ref.to_bits());
let closure = jsval.as_pointer::<u8>() 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)
}
20 changes: 20 additions & 0 deletions crates/perry-runtime/src/object/class_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,26 @@ pub unsafe extern "C" fn js_super_construct_apply(
);
}
}
// `class X extends Intl.<Ctor>` 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<f64> = 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
}

Expand Down
Loading
Loading