diff --git a/Cargo.lock b/Cargo.lock index 8f72b324fa..6c8a98c010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6310,6 +6310,7 @@ dependencies = [ "hostname", "icu_calendar", "icu_datetime", + "icu_locale", "icu_locale_core", "icu_time", "idna", diff --git a/changelog.d/8659-intl402-worklist.md b/changelog.d/8659-intl402-worklist.md new file mode 100644 index 0000000000..7466b856cf --- /dev/null +++ b/changelog.d/8659-intl402-worklist.md @@ -0,0 +1 @@ +Completed the #5896 Test262 Intl402 worklist. Locale canonicalization now applies ICU4X CLDR aliases and likely-subtag data, Intl constructors consistently handle proxy-backed locale and option objects, Collator/PluralRules/RelativeTimeFormat/Segmenter behavior matches the listed ECMA-402 cases, derived Intl classes preserve their native prototypes, and maximum-length arrays stay logically sparse. All 101 pinned worklist tests now pass. diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index 3ac80518dd..f02ee16750 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -179,16 +179,13 @@ global-webfetch = [] # detection would make `process.send` undefined, so err toward enabling). proc-ipc = [] # `Intl.getCanonicalLocales` / `*.supportedLocalesOf` BCP-47 (UTS #35) language-tag -# canonicalization via `icu_locale_core` (the data-free structural parser — case -# normalization, variant ordering, extension well-formedness, UTS35 rejection of -# extlang/grandfathered/duplicate-singleton tags). No CLDR data is pulled (that -# would need `icu_locale` + `icu_locale_data`), so deep alias replacement -# (grandfathered→preferred, complex subtag replacement) is out of scope. Only -# `intl.rs` uses it; a program that never canonicalizes a locale links none of -# it (the compiler enables it on `Intl.getCanonicalLocales`/`supportedLocalesOf` -# usage). Already pulled transitively by `temporal`, so default/shipped builds -# carry no extra weight. A hand-rolled structural fallback covers the off case. -intl-locale = ["dep:icu_locale_core"] +# canonicalization via ICU4X's structural parser plus compiled CLDR aliases and +# likely-subtag data. Only Intl locale operations use it; a program that never +# canonicalizes or expands a locale links none of it. The same data is already +# pulled transitively by `intl-datetime` in default builds, so the shipped full +# runtime carries no duplicate tables. A hand-rolled fallback covers the off +# case for size-optimized builds. +intl-locale = ["dep:icu_locale", "dep:icu_locale_core"] # CLDR-accurate Intl.DateTimeFormat / toLocaleString date-time patterns. intl-datetime = ["dep:icu_datetime", "dep:icu_time", "dep:icu_calendar", "dep:icu_locale_core"] # `full` only opt-ins the small Node-API helpers (os.hostname / os.homedir). @@ -302,9 +299,10 @@ unicode-normalization = { version = "0.1", optional = true } # Intl.Segmenter (the grapheme path is what string-width@7+/wrap-ansi@9+ use, # so it gates ink). Pure-Rust UAX #29 implementation, already in our lock graph. unicode-segmentation = { version = "1", optional = true } -# #5298: BCP-47 (UTS #35) structural locale-tag canonicalization for -# `Intl.getCanonicalLocales` / `*.supportedLocalesOf`. The data-free structural -# parser only (no CLDR alias tables); already in our lock graph via temporal_rs. +# #5298/#5896: BCP-47 (UTS #35) structural locale-tag canonicalization, CLDR +# aliases, and likely-subtag expansion for the Intl locale APIs. Both crates and +# their compiled data are already in the default lock graph via icu_datetime. +icu_locale = { version = "2", optional = true } icu_locale_core = { version = "2", optional = true } # CLDR date/time formatting for Intl.DateTimeFormat / Date.prototype.toLocale* # (icu4x 2.x, matching the icu_calendar/icu_locale_core already in the graph). diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index a86a4c6748..65e31deeb4 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -974,7 +974,36 @@ pub extern "C" fn js_array_set_length(arr: *mut ArrayHeader, new_length: f64) { // index 0 cannot remain observable after its getter truncates the // array to zero. If a non-configurable index blocks deletion, keep // that index and restore length to index + 1 per §10.4.2.4. - for i in (n..cur).rev() { + // + // A large logical extension does not allocate its holes (see the + // growth branch below), so do not walk those holes when the length + // is restored. Far materialized indices live in the named-property + // table. Delete them first, in the same descending order required + // by ArraySetLength, then visit the allocated dense prefix. + let capacity = (*arr).capacity; + if cur > capacity { + let mut sparse_indices: Vec = array_named_property_names(arr, false) + .into_iter() + .filter_map(|name| { + let index = name.parse::().ok()?; + (index != u32::MAX + && index >= n.max(capacity) + && index < cur + && index.to_string() == name) + .then_some(index) + }) + .collect(); + sparse_indices.sort_unstable_by(|a, b| b.cmp(a)); + sparse_indices.dedup(); + for i in sparse_indices { + if js_array_delete(arr, i) == 0 { + (*arr).length = i + 1; + refresh_array_numeric_layout(arr); + return; + } + } + } + for i in (n..cur.min(capacity)).rev() { if js_array_delete(arr, i) == 0 { (*arr).length = i + 1; refresh_array_numeric_layout(arr); @@ -984,6 +1013,15 @@ pub extern "C" fn js_array_set_length(arr: *mut ArrayHeader, new_length: f64) { (*arr).length = n; refresh_array_numeric_layout(arr); } else if n > cur { + // Growing `length` creates holes conceptually; it must not allocate + // a dense backing store proportional to the requested length. + // Test262's descriptor probe writes 2^32-1 here. Keep large sparse + // extensions logical and let later indexed writes choose storage. + if n > (*arr).capacity && n > 1_000_000 { + (*arr).length = n; + refresh_array_numeric_layout(arr); + return; + } // Extend: pad with TAG_HOLE. Past-capacity extensions go // through `js_array_grow` which installs a forwarding pointer at // the OLD location (issue #233 mechanism), so the caller's stale diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs index 11f1e32e8e..151e794cbf 100644 --- a/crates/perry-runtime/src/array/tests.rs +++ b/crates/perry-runtime/src/array/tests.rs @@ -1187,6 +1187,20 @@ fn test_numeric_array_layout_length_and_delete_transitions() { } } +#[test] +fn large_length_growth_stays_logically_sparse() { + let mut arr = js_array_alloc(1); + arr = js_array_push_f64(arr, 1.0); + js_array_set_length(arr, u32::MAX as f64); + + assert_eq!(js_array_length(arr), u32::MAX); + assert!(unsafe { (*arr).capacity } <= 1_000_000); + + js_array_set_length(arr, 1.0); + assert_eq!(js_array_length(arr), 1); + assert_eq!(array_spec_get(arr, 0), 1.0); +} + #[test] fn test_numeric_array_layout_immutable_helpers_preserve_or_downgrade() { let values = [10.0, 2.0, 30.0, 40.0]; diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index 57f26a4a3d..0a003e2870 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -40,6 +40,17 @@ mod time_zone; pub(crate) use time_zone::resolved_date_time_zone; mod install; use install::install_constructor; +mod method_install; +use method_install::{ + install_bound_instance_function, install_bound_instance_function_from_handle, install_function, + install_function_from_handle, +}; +mod rooted_fields; +use rooted_fields::{ + get_field, get_field_from_raw_handle, get_field_from_value_handle, get_number_field, + get_option_value, get_string_field, get_string_field_from_raw_handle, set_builtin_attrs, + set_field, set_internal_field, set_internal_field_from_raw_handle, +}; mod subclass; pub(crate) use subclass::{intl_instanceof, intl_subclass_super, is_intl_constructor_value}; use subclass::{locale_instance_tag, push_locale_element}; @@ -61,18 +72,16 @@ pub(crate) use date_collator::{ date_time_format_bound_to_parts_thunk, date_time_format_format_getter_thunk, date_time_format_range_thunk, date_time_format_range_to_parts_thunk, date_time_format_resolved_options_thunk, date_time_format_to_parts_thunk, - temporal_locale_string, TemporalLocaleCtx, + resolve_collator_locale, temporal_locale_string, TemporalLocaleCtx, }; pub(crate) use list_relative_plural::{ canonicalize_calendar_id, canonicalize_offset_time_zone, is_valid_offset_time_zone, list_format_bound_format_thunk, list_format_bound_resolved_options_thunk, list_format_bound_to_parts_thunk, list_format_format_thunk, list_format_parts, list_format_resolved_options_thunk, list_format_to_parts_thunk, - plural_rules_bound_resolved_options_thunk, plural_rules_bound_select_range_thunk, - plural_rules_bound_select_thunk, plural_rules_resolved_options_thunk, - plural_rules_select_range_thunk, plural_rules_select_thunk, rtf_bound_format_thunk, - rtf_bound_resolved_options_thunk, rtf_bound_to_parts_thunk, rtf_format_thunk, - rtf_resolved_options_thunk, rtf_to_parts_thunk, + plural_rules_resolved_options_thunk, plural_rules_select_range_thunk, + plural_rules_select_thunk, rtf_bound_format_thunk, rtf_bound_resolved_options_thunk, + rtf_bound_to_parts_thunk, rtf_format_thunk, rtf_resolved_options_thunk, rtf_to_parts_thunk, }; pub(crate) use number_format::{ bigint_to_locale_string, captured_intl_object, nf_resolved_default, @@ -137,6 +146,7 @@ const KEY_TYPE: &str = "__intlType"; const KEY_LF_STYLE: &str = "__intlListStyle"; const KEY_NUMERIC: &str = "__intlNumeric"; const KEY_RTF_STYLE: &str = "__intlRtfStyle"; +const KEY_RTF_NUMBERING: &str = "__intlRtfNumbering"; const KEY_PR_MIN_INT: &str = "__intlMinInt"; const KEY_PR_MIN_FRAC: &str = "__intlMinFrac"; const KEY_PR_MAX_FRAC: &str = "__intlMaxFrac"; @@ -251,46 +261,6 @@ fn array_ptr_from_value(value: f64) -> Option<*const crate::ArrayHeader> { (!ptr.is_null()).then_some(ptr) } -fn get_field(value: *const ObjectHeader, key: &str) -> f64 { - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - js_object_get_field_by_name_f64(value, key_ptr) -} - -fn set_field(obj: *mut ObjectHeader, key: &str, value: f64) { - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - js_object_set_field_by_name(obj, key_ptr, value); -} - -fn set_builtin_attrs(obj: *mut ObjectHeader, key: &str, attrs: PropertyAttrs) { - set_builtin_property_attrs(obj as usize, key.to_string(), attrs); -} - -fn set_internal_field(obj: *mut ObjectHeader, key: &str, value: f64) { - set_field(obj, key, value); - set_builtin_attrs(obj, key, PropertyAttrs::new(true, false, true)); -} - -fn get_string_field(obj: *const ObjectHeader, key: &str) -> Option { - string_from_string_value(get_field(obj, key)) -} - -fn get_number_field(obj: *const ObjectHeader, key: &str) -> Option { - let value = get_field(obj, key); - let js = JSValue::from_bits(value.to_bits()); - if js.is_undefined() || js.is_null() { - None - } else { - Some(js.to_number()) - } -} - -fn get_option_value(options: f64, key: &str) -> f64 { - let Some(obj) = object_ptr_from_value(options) else { - return undefined(); - }; - get_field(obj, key) -} - /// Coerce an already-fetched option value to its GetOption string form. ECMA-402 /// GetOption treats ONLY `undefined` as "absent → fallback"; every other value — /// `null` included — is coerced with ToString and then checked against the @@ -519,22 +489,26 @@ pub(crate) fn canonical_locale(tag: &str) -> Option { /// canonicalization. Returns `None` when the tag is not a structurally valid /// `unicode_locale_id` (the caller raises `RangeError`). /// -/// With the `intl-locale` feature this delegates to `icu_locale_core`'s data-free -/// structural parser, which gives correct case normalization, variant ordering, -/// extension well-formedness, and UTS #35 rejection of extlang / grandfathered / -/// duplicate-singleton tags. (Deep CLDR alias replacement — -/// grandfathered→preferred, complex subtag replacement, unicode-extension value -/// aliases — needs `icu_locale` + its CLDR data and is out of scope.) The -/// fallback path uses the lighter hand-rolled `canonical_locale`. +/// With the `intl-locale` feature this delegates to ICU4X's structural parser +/// and compiled CLDR canonicalizer, which cover case/variant/extension +/// normalization as well as language, script, region, variant, and transformed +/// extension aliases. Perry's small post-pass supplies the handful of Unicode +/// extension type aliases that ICU4X does not currently include. The fallback +/// path uses the lighter hand-rolled `canonical_locale`. fn canonicalize_language_tag(tag: &str) -> Option { #[cfg(feature = "intl-locale")] { - match icu_locale_core::Locale::normalize(tag) { - Ok(canonical) => Some(canonicalize_unicode_extension_types( - &canonical.into_owned(), - )), - Err(_) => None, - } + let mut locale = match tag.parse::() { + Ok(locale) => locale, + Err(_) + if (5..=8).contains(&tag.len()) && tag.bytes().all(|b| b.is_ascii_alphabetic()) => + { + return Some(tag.to_ascii_lowercase()); + } + Err(_) => return None, + }; + icu_locale::LocaleCanonicalizer::new_extended().canonicalize(&mut locale); + Some(canonicalize_unicode_extension_types(&locale.to_string())) } #[cfg(not(feature = "intl-locale"))] { @@ -542,15 +516,27 @@ fn canonicalize_language_tag(tag: &str) -> Option { } } -/// `HasProperty(O, ToString(index))` — true when the integer-indexed property is -/// present (own or inherited). Used to skip holes/absent indices in -/// CanonicalizeLocaleList's array/array-like walk. -fn js_has_index(obj: f64, index: u32) -> bool { - let key = string_value(&index.to_string()); - crate::object::js_object_has_property(obj, key).to_bits() == crate::value::TAG_TRUE +/// CanonicalizeLocaleList's `HasProperty(O, ToString(index))`. +fn js_has_index(obj: &crate::gc::RuntimeHandle<'_>, index: u32) -> bool { + let scope = crate::gc::RuntimeHandleScope::new(); + let key = scope.root_nanbox_f64(string_value(&index.to_string())); + if crate::proxy::js_proxy_is_proxy(obj.get_nanbox_f64()) != 0 { + return crate::proxy::js_proxy_has(obj.get_nanbox_f64(), key.get_nanbox_f64()).to_bits() + == crate::value::TAG_TRUE; + } + crate::object::js_object_has_property(obj.get_nanbox_f64(), key.get_nanbox_f64()).to_bits() + == crate::value::TAG_TRUE +} + +fn proxy_get_from_value_handle(value: &crate::gc::RuntimeHandle<'_>, key: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let key = scope.root_nanbox_f64(string_value(key)); + crate::proxy::js_proxy_get(value.get_nanbox_f64(), key.get_nanbox_f64()) } fn locales_from_value(locales: f64) -> Vec { + let scope = crate::gc::RuntimeHandleScope::new(); + let locales_handle = scope.root_nanbox_f64(locales); let js = JSValue::from_bits(locales.to_bits()); // CanonicalizeLocaleList(undefined) is the empty list; `null` fails ToObject // with a TypeError (everything else is a String or coerces via ToObject). @@ -568,29 +554,57 @@ fn locales_from_value(locales: f64) -> Vec { }; 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. + // A Proxy must be classified before probing Locale/Array/Object headers: + // those probes reinterpret pointer payloads and a Proxy has a distinct GC + // layout. CanonicalizeLocaleList observes it through [[Get]]/[[HasProperty]] + // regardless of the target's underlying kind. + if crate::proxy::js_proxy_is_proxy(locales_handle.get_nanbox_f64()) != 0 { + let len = crate::builtins::js_number_coerce(proxy_get_from_value_handle( + &locales_handle, + "length", + )); + let mut out = Vec::new(); + for i in 0..if len.is_finite() && len > 0.0 { + len as u32 + } else { + 0 + } { + if js_has_index(&locales_handle, i) { + push_locale_element( + &mut out, + proxy_get_from_value_handle(&locales_handle, &i.to_string()), + ); + } + } + return out; + } + // An Intl.Locale contributes its internal locale instead of being iterated. 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) { + if let Some(arr) = array_ptr_from_value(locales_handle.get_nanbox_f64()) { let len = js_array_length(arr); let mut out = Vec::with_capacity(len as usize); for i in 0..len { + if !js_has_index(&locales_handle, i) { + continue; + } + let Some(arr) = array_ptr_from_value(locales_handle.get_nanbox_f64()) else { + break; + }; push_locale_element(&mut out, js_array_get_f64(arr, i)); } return out; } // CanonicalizeLocaleList on a generic array-like Object: iterate `O[0..length]` // (e.g. `{ 0: "DE", length: 1 }` → `["de"]`). - if let Some(obj) = object_ptr_from_value(locales) { + if object_ptr_from_value(locales_handle.get_nanbox_f64()).is_some() { // `length = ? ToLength(? Get(O, "length"))`: a throwing `length` getter or // ToNumber step (Symbol / abrupt valueOf/toString) propagates here. - let len_raw = get_field(obj, "length"); + let len_raw = get_field_from_value_handle(&locales_handle, "length"); let len_num = crate::builtins::js_number_coerce(len_raw); let len = if len_num.is_finite() && len_num > 0.0 { len_num as u32 @@ -601,16 +615,42 @@ fn locales_from_value(locales: f64) -> Vec { for i in 0..len { // Skip absent indices (`HasProperty` is false) — e.g. // `{ length: 3, 0: "en" }` yields just `["en"]`, never `undefined`. - if !js_has_index(locales, i) { + if !js_has_index(&locales_handle, i) { continue; } - push_locale_element(&mut out, get_field(obj, &i.to_string())); + push_locale_element( + &mut out, + get_field_from_value_handle(&locales_handle, &i.to_string()), + ); } return out; } - // Other primitives (number/boolean/Symbol/BigInt): ToObject yields a wrapper - // with length 0 — an empty list, no throw. - Vec::new() + // Other primitives (number/boolean/Symbol/BigInt): CanonicalizeLocaleList + // applies ToObject, so inherited `length` / indexed getters on the wrapper + // prototype remain observable (DisplayNames/locales-symbol-length.js). + let boxed = scope.root_nanbox_f64(crate::object::js_object_coerce( + locales_handle.get_nanbox_f64(), + )); + if object_ptr_from_value(boxed.get_nanbox_f64()).is_none() { + return Vec::new(); + } + let len_raw = get_field_from_value_handle(&boxed, "length"); + let len_num = crate::builtins::js_number_coerce(len_raw); + let len = if len_num.is_finite() && len_num > 0.0 { + len_num as u32 + } else { + 0 + }; + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + if js_has_index(&boxed, i) { + push_locale_element( + &mut out, + get_field_from_value_handle(&boxed, &i.to_string()), + ); + } + } + out } /// BestAvailableLocale (lookup) — a requested canonical locale is "supported" @@ -872,27 +912,19 @@ fn enum_option_strict(options: f64, key: &str, allowed: &[&str], default: &str) } } -/// `GetOptionsObject(options)`: `undefined` yields an empty bag (reported as -/// `undefined`, which the option readers treat as "every key absent"); an Object -/// passes through unchanged; any other value (including `null`, primitives, and -/// BigInt) throws `TypeError`. Used by the constructors whose spec step is -/// `GetOptionsObject` (ListFormat, Segmenter, PluralRules, …). +/// ECMA-402 GetOptionsObject. fn get_options_object(options: f64) -> f64 { let jv = JSValue::from_bits(options.to_bits()); if jv.is_undefined() { return options; } - if object_ptr_from_value(options).is_some() { + if crate::proxy::js_proxy_is_proxy(options) != 0 || object_ptr_from_value(options).is_some() { return options; } throw_type_error("Cannot convert undefined or null to object"); } -/// `CoerceOptionsToObject(options)` partial: `undefined` stays an empty bag and -/// `null` throws `TypeError` (`ToObject(null)`). Primitives are *not* boxed here -/// — Perry reads option keys directly off Objects, so a primitive simply yields -/// every-key-absent — but `null` must still reject. Used by the constructors -/// whose spec step is `ToObject` (RelativeTimeFormat, Collator, …). +/// CoerceOptionsToObject's null rejection; callers box primitives when needed. fn coerce_options_reject_null(options: f64) -> f64 { if JSValue::from_bits(options.to_bits()).is_null() { throw_type_error("Cannot convert undefined or null to object"); @@ -900,19 +932,11 @@ fn coerce_options_reject_null(options: f64) -> f64 { options } -/// `ToObject(options)` for the SupportedLocales option read: `null` / `undefined` -/// are handled by the caller; a non-object primitive (Boolean, Number, String, -/// Symbol, BigInt) is boxed into a fresh empty object so that reading an option -/// key walks the standard prototype chain and fires any `Object.prototype` -/// getter for that key exactly once (SupportedLocales step 1.a, test262 -/// `supportedLocalesOf/options-toobject.js`). A real object passes through. +/// Box a primitive so inherited Object.prototype option getters stay observable. fn to_object_for_options(options: f64) -> f64 { - if object_ptr_from_value(options).is_some() { + if crate::proxy::js_proxy_is_proxy(options) != 0 || object_ptr_from_value(options).is_some() { return options; } - // Box the primitive: an empty object has no own option keys, so every read - // resolves through the prototype chain — matching the boxed-wrapper behaviour - // the spec observes (the wrapper carries no `localeMatcher` of its own). js_nanbox_pointer(js_object_alloc(0, 0) as i64) } @@ -936,6 +960,8 @@ fn dt_component_option( allowed: &[&str], store_key: &str, ) -> bool { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(obj); match get_option_string(options, key) { None => false, Some(value) => { @@ -944,12 +970,22 @@ fn dt_component_option( "Value {value} out of range for Intl options property {key}" )); } - set_internal_field(obj, store_key, string_value(&value)); + set_internal_field_from_raw_handle(&obj, store_key, string_value(&value)); true } } } +fn dt_component_option_from_handle( + obj: &crate::gc::RuntimeHandle<'_>, + options: f64, + key: &str, + allowed: &[&str], + store_key: &str, +) -> bool { + obj.with_mut_ptr(|obj| dt_component_option(obj, options, key, allowed, store_key)) +} + /// Validate a *named* (non-offset) `timeZone` identifier. Perry ships no tz /// database (see `date.rs`), so this is a structural check rather than a lookup: /// the case-insensitive UTC aliases normalize to `"UTC"`, the legacy @@ -959,14 +995,23 @@ fn dt_component_option( /// (`"MEZ"`, `"invalid"`, `"Europe/İstanbul"`, …) do not. Returns the (best /// effort, un-recased) canonical identifier, or `None` to signal `RangeError`. fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, options: f64) -> f64 { - let locale = locale_or_default(locales); + // Locale/option access can invoke user Proxy traps. Keep both arguments and + // the partially initialized result live across those calls; the handles are + // refreshed explicitly in the long PluralRules read-order sequence below. + let scope = crate::gc::RuntimeHandleScope::new(); + let closure_handle = scope.root_raw_const_ptr(closure); + let locales_handle = scope.root_nanbox_f64(locales); + let options_handle = scope.root_nanbox_f64(options); + let locale = locale_or_default(locales_handle.get_nanbox_f64()); let obj = js_object_alloc(0, 8); - set_internal_field(obj, KEY_KIND, string_value(kind)); - set_internal_field(obj, KEY_LOCALE, string_value(&locale)); + let obj_handle = scope.root_raw_mut_ptr(obj); + set_internal_field_from_raw_handle(&obj_handle, KEY_KIND, string_value(kind)); + set_internal_field_from_raw_handle(&obj_handle, KEY_LOCALE, string_value(&locale)); + let current_options = || options_handle.get_nanbox_f64(); match kind { KIND_NUMBER => { - configure_number_format(obj, &locale, options); + obj_handle.with_mut_ptr(|obj| configure_number_format(obj, &locale, current_options())); // The bound format function is the [[BoundFormat]] slot: ECMA-402 // gives it an empty `name` ("") and length 1. It is installed as an // own `format` property so `nf.format(x)` dispatches without the @@ -974,22 +1019,22 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // props), and is also stashed in the hidden KEY_NF_BOUND_FORMAT slot // that the prototype `format` getter reads — so mutating or deleting // the public property can't corrupt what the accessor returns. - let format_fn = install_bound_instance_function( - obj, + let format_fn = install_bound_instance_function_from_handle( + &obj_handle, "format", number_format_bound_format_thunk as *const u8, 1, ); if !format_fn.is_null() { crate::object::set_bound_native_closure_name(format_fn, ""); - set_internal_field( - obj, + set_internal_field_from_raw_handle( + &obj_handle, KEY_NF_BOUND_FORMAT, js_nanbox_pointer(format_fn as i64), ); } - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "formatToParts", number_format_bound_to_parts_thunk as *const u8, 1, @@ -1001,24 +1046,24 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // 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, + install_function_from_handle( + &obj_handle, "formatRange", number_format_range_thunk as *const u8, 2, 2, false, ); - install_function( - obj, + install_function_from_handle( + &obj_handle, "formatRangeToParts", number_format_range_to_parts_thunk as *const u8, 2, 2, false, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "resolvedOptions", number_format_bound_resolved_options_thunk as *const u8, 0, @@ -1031,7 +1076,7 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // with no DateTimeFormat-relevant properties, i.e. behave as empty — // `object_ptr_from_value` already returns `None` for them, so option // reads simply see `undefined`. - if JSValue::from_bits(options.to_bits()).is_null() { + if JSValue::from_bits(current_options().to_bits()).is_null() { throw_type_error("Cannot convert undefined or null to object"); } // GetOption reads run in the exact ECMA-402 CreateDateTimeFormat @@ -1039,18 +1084,20 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // localeMatcher / formatMatcher are validated but don't affect the // deterministic formatter, so their resolved value is discarded. let _ = enum_option( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", ); // `calendar` must match the Unicode locale `type` nonterminal; store // the canonicalized ID so `resolvedOptions().calendar` reflects it. - if let Some(calendar) = get_locale_extension_option(options, "calendar") { + if let Some(calendar) = get_locale_extension_option(current_options(), "calendar") { match canonicalize_calendar_id(&calendar) { - Some(canonical) => { - set_internal_field(obj, KEY_CALENDAR, string_value(&canonical)) - } + Some(canonical) => set_internal_field_from_raw_handle( + &obj_handle, + KEY_CALENDAR, + string_value(&canonical), + ), None => throw_range_error(&format!( "Value {calendar} out of range for Intl options property calendar" )), @@ -1061,30 +1108,35 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // then run ResolveLocale for `nu` — reconciling the option with the // locale's `-u-nu-` keyword so `resolvedOptions().locale` / // `.numberingSystem` reflect only the supported value actually used. - let dtf_opt_ns = get_locale_extension_option(options, "numberingSystem").map(|ns| { - if !is_well_formed_numbering_system(&ns) { - throw_range_error(&format!( - "Value {ns} out of range for Intl options property numberingSystem" - )); - } - ns.to_ascii_lowercase() - }); + let dtf_opt_ns = + get_locale_extension_option(current_options(), "numberingSystem").map(|ns| { + if !is_well_formed_numbering_system(&ns) { + throw_range_error(&format!( + "Value {ns} out of range for Intl options property numberingSystem" + )); + } + ns.to_ascii_lowercase() + }); let (dtf_locale, dtf_numbering) = resolve_numbering_system(&locale, dtf_opt_ns.as_deref()); - set_internal_field(obj, KEY_LOCALE, string_value(&dtf_locale)); - set_internal_field(obj, KEY_NUMBERING_SYSTEM, string_value(&dtf_numbering)); + set_internal_field_from_raw_handle(&obj_handle, KEY_LOCALE, string_value(&dtf_locale)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NUMBERING_SYSTEM, + string_value(&dtf_numbering), + ); // hour12 (boolean) then hourCycle (enum) — both only surface in // `resolvedOptions` when the resolved pattern has an hour field. - if let Some(h12) = get_bool_option(options, "hour12") { - set_internal_field(obj, KEY_HOUR12, bool_value(h12)); + if let Some(h12) = get_bool_option(current_options(), "hour12") { + set_internal_field_from_raw_handle(&obj_handle, KEY_HOUR12, bool_value(h12)); } - if let Some(hc) = get_option_string(options, "hourCycle") { + if let Some(hc) = get_option_string(current_options(), "hourCycle") { if !["h11", "h12", "h23", "h24"].contains(&hc.as_str()) { throw_range_error(&format!( "Value {hc} out of range for Intl options property hourCycle" )); } - set_internal_field(obj, KEY_HOUR_CYCLE, string_value(&hc)); + set_internal_field_from_raw_handle(&obj_handle, KEY_HOUR_CYCLE, string_value(&hc)); } // ECMA-402 DefaultTimeZone(): when no `timeZone` option is given, use // the HOST time zone (Node returns e.g. "Europe/Berlin"), not UTC — @@ -1093,8 +1145,12 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // single source of that logic (it canonicalizes offsets to `±HH:mm` // for FormatOffsetTimeZoneIdentifier and validates named zones // structurally, Perry having no tz database). - let time_zone = resolved_date_time_zone(options); - set_internal_field(obj, KEY_TIME_ZONE, string_value(&time_zone)); + let time_zone = resolved_date_time_zone(current_options()); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_TIME_ZONE, + string_value(&time_zone), + ); // Date/time component options (ECMA-402 Table 7), read in order. Each // out-of-range value is a RangeError. // @@ -1113,9 +1169,9 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // conflict check (step 35.b: throw if style + any component option). let mut any_component = false; let mut has_explicit_component = false; - let has_weekday = dt_component_option( - obj, - options, + let has_weekday = dt_component_option_from_handle( + &obj_handle, + current_options(), "weekday", &["narrow", "short", "long"], KEY_WEEKDAY, @@ -1123,58 +1179,89 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option any_component |= has_weekday; has_explicit_component |= has_weekday; // era counts toward the style-conflict check but NOT toward needDefaults. - has_explicit_component |= - dt_component_option(obj, options, "era", &["narrow", "short", "long"], KEY_ERA); - let has_year = - dt_component_option(obj, options, "year", &["2-digit", "numeric"], KEY_YEAR); + has_explicit_component |= dt_component_option_from_handle( + &obj_handle, + current_options(), + "era", + &["narrow", "short", "long"], + KEY_ERA, + ); + let has_year = dt_component_option_from_handle( + &obj_handle, + current_options(), + "year", + &["2-digit", "numeric"], + KEY_YEAR, + ); any_component |= has_year; has_explicit_component |= has_year; - let has_month = dt_component_option( - obj, - options, + let has_month = dt_component_option_from_handle( + &obj_handle, + current_options(), "month", &["2-digit", "numeric", "narrow", "short", "long"], KEY_MONTH, ); any_component |= has_month; has_explicit_component |= has_month; - let has_day = - dt_component_option(obj, options, "day", &["2-digit", "numeric"], KEY_DAY); + let has_day = dt_component_option_from_handle( + &obj_handle, + current_options(), + "day", + &["2-digit", "numeric"], + KEY_DAY, + ); any_component |= has_day; has_explicit_component |= has_day; - let has_day_period = dt_component_option( - obj, - options, + let has_day_period = dt_component_option_from_handle( + &obj_handle, + current_options(), "dayPeriod", &["narrow", "short", "long"], KEY_DAY_PERIOD, ); any_component |= has_day_period; has_explicit_component |= has_day_period; - let has_hour = - dt_component_option(obj, options, "hour", &["2-digit", "numeric"], KEY_HOUR); + let has_hour = dt_component_option_from_handle( + &obj_handle, + current_options(), + "hour", + &["2-digit", "numeric"], + KEY_HOUR, + ); any_component |= has_hour; has_explicit_component |= has_hour; - let has_minute = - dt_component_option(obj, options, "minute", &["2-digit", "numeric"], KEY_MINUTE); + let has_minute = dt_component_option_from_handle( + &obj_handle, + current_options(), + "minute", + &["2-digit", "numeric"], + KEY_MINUTE, + ); any_component |= has_minute; has_explicit_component |= has_minute; - let has_second = - dt_component_option(obj, options, "second", &["2-digit", "numeric"], KEY_SECOND); + let has_second = dt_component_option_from_handle( + &obj_handle, + current_options(), + "second", + &["2-digit", "numeric"], + KEY_SECOND, + ); any_component |= has_second; has_explicit_component |= has_second; // fractionalSecondDigits is GetNumberOption(1, 3) — out of range or // non-numeric is a RangeError. - if let Some(n) = get_number_option_coerced(options, "fractionalSecondDigits", 1.0, 3.0) + if let Some(n) = + get_number_option_coerced(current_options(), "fractionalSecondDigits", 1.0, 3.0) { - set_internal_field(obj, KEY_FRACTIONAL, n); + set_internal_field_from_raw_handle(&obj_handle, KEY_FRACTIONAL, n); any_component = true; has_explicit_component = true; } // timeZoneName counts toward the style-conflict check but NOT toward needDefaults. - has_explicit_component |= dt_component_option( - obj, - options, + has_explicit_component |= dt_component_option_from_handle( + &obj_handle, + current_options(), "timeZoneName", &[ "short", @@ -1186,10 +1273,15 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option ], KEY_TIME_ZONE_NAME, ); - let _ = enum_option(options, "formatMatcher", &["basic", "best fit"], "best fit"); + let _ = enum_option( + current_options(), + "formatMatcher", + &["basic", "best fit"], + "best fit", + ); // dateStyle / timeStyle have no default (an absent style stays absent // in `resolvedOptions`); an out-of-range value is a RangeError. - let date_style = get_option_string(options, "dateStyle"); + let date_style = get_option_string(current_options(), "dateStyle"); if let Some(ref ds) = date_style { if !["full", "long", "medium", "short"].contains(&ds.as_str()) { throw_range_error(&format!( @@ -1197,7 +1289,7 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option )); } } - let time_style = get_option_string(options, "timeStyle"); + let time_style = get_option_string(current_options(), "timeStyle"); if let Some(ref ts) = time_style { if !["full", "long", "medium", "short"].contains(&ts.as_str()) { throw_range_error(&format!( @@ -1214,54 +1306,58 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option ); } if let Some(ds) = date_style { - set_internal_field(obj, KEY_DATE_STYLE, string_value(&ds)); + set_internal_field_from_raw_handle(&obj_handle, KEY_DATE_STYLE, string_value(&ds)); } if let Some(ts) = time_style { - set_internal_field(obj, KEY_TIME_STYLE, string_value(&ts)); + set_internal_field_from_raw_handle(&obj_handle, KEY_TIME_STYLE, string_value(&ts)); } // ToDateTimeOptions(required="any", defaults="date"): when neither a // style nor any component was requested, fall back to numeric // year/month/day so `resolvedOptions` reports the default date shape. if !has_style && !any_component { - set_internal_field(obj, KEY_YEAR, string_value("numeric")); - set_internal_field(obj, KEY_MONTH, string_value("numeric")); - set_internal_field(obj, KEY_DAY, string_value("numeric")); - set_internal_field(obj, KEY_DT_IS_DEFAULT, bool_value(true)); + set_internal_field_from_raw_handle(&obj_handle, KEY_YEAR, string_value("numeric")); + set_internal_field_from_raw_handle(&obj_handle, KEY_MONTH, string_value("numeric")); + set_internal_field_from_raw_handle(&obj_handle, KEY_DAY, string_value("numeric")); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_DT_IS_DEFAULT, + bool_value(true), + ); } - let format_fn = install_bound_instance_function( - obj, + let format_fn = install_bound_instance_function_from_handle( + &obj_handle, "format", date_time_format_bound_format_thunk as *const u8, 1, ); if !format_fn.is_null() { crate::object::set_bound_native_closure_name(format_fn, ""); - set_internal_field( - obj, + set_internal_field_from_raw_handle( + &obj_handle, KEY_DTF_BOUND_FORMAT, js_nanbox_pointer(format_fn as i64), ); } - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "formatToParts", date_time_format_bound_to_parts_thunk as *const u8, 1, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "formatRange", date_time_format_bound_range_thunk as *const u8, 2, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "formatRangeToParts", date_time_format_bound_range_to_parts_thunk as *const u8, 2, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "resolvedOptions", date_time_format_bound_resolved_options_thunk as *const u8, 0, @@ -1272,10 +1368,10 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // TypeError) then GetOption in this exact order: usage, localeMatcher, // collation, numeric, caseFirst, sensitivity, ignorePunctuation // (constructor-options-throwing-getters / resolvedOptions order.js). - let options = coerce_options_reject_null(options); - let usage = enum_option_strict(options, "usage", &["sort", "search"], "sort"); + let _ = coerce_options_reject_null(current_options()); + let usage = enum_option_strict(current_options(), "usage", &["sort", "search"], "sort"); let _ = enum_option_strict( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", @@ -1284,71 +1380,79 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // /`search` values, are a RangeError (the latter are only valid as a // `usage` selector, never an explicit collation). A valid value wins // over any `-u-co-` keyword; absent ⇒ fall back to the extension. - let collation_opt = get_option_string_coerced(options, "collation").map(|v| { - if !is_well_formed_numbering_system(&v) || v == "standard" || v == "search" { - throw_range_error(&format!( - "Value {v} out of range for Intl options property collation" - )); - } - v - }); - let numeric_opt = get_bool_option(options, "numeric"); - let case_first_opt = get_option_string_coerced(options, "caseFirst").map(|v| { - if ["upper", "lower", "false"].contains(&v.as_str()) { + let collation_opt = + get_option_string_coerced(current_options(), "collation").map(|v| { + if !is_well_formed_numbering_system(&v) || v == "standard" || v == "search" { + throw_range_error(&format!( + "Value {v} out of range for Intl options property collation" + )); + } v - } else { - throw_range_error(&format!( - "Value {v} out of range for Intl options property caseFirst" - )) - } - }); + }); + let numeric_opt = get_bool_option(current_options(), "numeric"); + let case_first_opt = + get_option_string_coerced(current_options(), "caseFirst").map(|v| { + if ["upper", "lower", "false"].contains(&v.as_str()) { + v + } else { + throw_range_error(&format!( + "Value {v} out of range for Intl options property caseFirst" + )) + } + }); let sensitivity = enum_option_strict( - options, + current_options(), "sensitivity", &["base", "accent", "case", "variant"], "variant", ); - let ignore_punct = get_bool_option(options, "ignorePunctuation").unwrap_or(false); - // ResolveLocale: when an option is absent, fall back to the matching - // Unicode (`-u-`) extension keyword in the resolved locale — `kn` - // (numeric, value-less ⇒ true) and `kf` (caseFirst). - let numeric = - numeric_opt.unwrap_or_else(|| match unicode_extension_keyword(&locale, "kn") { - Some(v) => v != "false", - None => false, - }); - let case_first = case_first_opt.unwrap_or_else(|| { - unicode_extension_keyword(&locale, "kf") - .filter(|v| ["upper", "lower", "false"].contains(&v.as_str())) - .unwrap_or_else(|| "false".to_string()) - }); - let collation = collation_opt.unwrap_or_else(|| { - unicode_extension_keyword(&locale, "co") - .filter(|v| !v.is_empty() && v != "standard" && v != "search") - .unwrap_or_else(|| "default".to_string()) - }); - set_internal_field(obj, KEY_COL_USAGE, string_value(&usage)); - set_internal_field(obj, KEY_COL_SENSITIVITY, string_value(&sensitivity)); - set_internal_field(obj, KEY_COL_IGNORE_PUNCT, bool_value(ignore_punct)); - set_internal_field(obj, KEY_COL_COLLATION, string_value(&collation)); - set_internal_field(obj, KEY_COL_NUMERIC, bool_value(numeric)); - set_internal_field(obj, KEY_COL_CASE_FIRST, string_value(&case_first)); - let compare_fn = install_bound_instance_function( - obj, + let ignore_punct = get_bool_option(current_options(), "ignorePunctuation") + .unwrap_or_else(|| locale == "th" || locale.starts_with("th-")); + let (resolved_locale, collation, numeric, case_first) = + resolve_collator_locale(&locale, collation_opt, numeric_opt, case_first_opt); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_LOCALE, + string_value(&resolved_locale), + ); + set_internal_field_from_raw_handle(&obj_handle, KEY_COL_USAGE, string_value(&usage)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_COL_SENSITIVITY, + string_value(&sensitivity), + ); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_COL_IGNORE_PUNCT, + bool_value(ignore_punct), + ); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_COL_COLLATION, + string_value(&collation), + ); + set_internal_field_from_raw_handle(&obj_handle, KEY_COL_NUMERIC, bool_value(numeric)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_COL_CASE_FIRST, + string_value(&case_first), + ); + let compare_fn = install_bound_instance_function_from_handle( + &obj_handle, "compare", collator_bound_compare_thunk as *const u8, 2, ); if !compare_fn.is_null() { crate::object::set_bound_native_closure_name(compare_fn, ""); - set_internal_field( - obj, + set_internal_field_from_raw_handle( + &obj_handle, KEY_COL_BOUND_COMPARE, js_nanbox_pointer(compare_fn as i64), ); } - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "resolvedOptions", collator_bound_resolved_options_thunk as *const u8, 0, @@ -1357,24 +1461,28 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option KIND_SEGMENTER => { // `? ToObject(options)` (null → TypeError), then GetOption in order: // localeMatcher, granularity (options-order.js / options-null.js). - let options = coerce_options_reject_null(options); + let _ = coerce_options_reject_null(current_options()); let _ = enum_option_strict( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", ); let granularity = - normalize_granularity(get_option_string_coerced(options, "granularity")); - set_internal_field(obj, KEY_GRANULARITY, string_value(&granularity)); - install_bound_instance_function( - obj, + normalize_granularity(get_option_string_coerced(current_options(), "granularity")); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_GRANULARITY, + string_value(&granularity), + ); + install_bound_instance_function_from_handle( + &obj_handle, "segment", segmenter_bound_segment_thunk as *const u8, 1, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "resolvedOptions", segmenter_bound_resolved_options_thunk as *const u8, 0, @@ -1384,36 +1492,41 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // `? GetOptionsObject(options)` (any non-Object, non-undefined → // TypeError), then GetOption: localeMatcher, type, style // (options-getoptionsobject.js / options-order.js). - let options = get_options_object(options); + let _ = get_options_object(current_options()); let _ = enum_option_strict( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", ); let list_type = enum_option_strict( - options, + current_options(), "type", &["conjunction", "disjunction", "unit"], "conjunction", ); - let style = enum_option_strict(options, "style", &["long", "short", "narrow"], "long"); - set_internal_field(obj, KEY_TYPE, string_value(&list_type)); - set_internal_field(obj, KEY_LF_STYLE, string_value(&style)); - install_bound_instance_function( - obj, + let style = enum_option_strict( + current_options(), + "style", + &["long", "short", "narrow"], + "long", + ); + set_internal_field_from_raw_handle(&obj_handle, KEY_TYPE, string_value(&list_type)); + set_internal_field_from_raw_handle(&obj_handle, KEY_LF_STYLE, string_value(&style)); + install_bound_instance_function_from_handle( + &obj_handle, "format", list_format_bound_format_thunk as *const u8, 1, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "formatToParts", list_format_bound_to_parts_thunk as *const u8, 1, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "resolvedOptions", list_format_bound_resolved_options_thunk as *const u8, 0, @@ -1422,165 +1535,104 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option KIND_RELATIVE_TIME => { // `? ToObject(options)` (null → TypeError), then GetOption in order: // localeMatcher, numberingSystem, style, numeric (options-order.js). - let options = coerce_options_reject_null(options); + options_handle.set_nanbox_f64(to_object_for_options(coerce_options_reject_null( + current_options(), + ))); let _ = enum_option_strict( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", ); - if let Some(ns) = get_option_string_coerced(options, "numberingSystem") { - if !is_well_formed_numbering_system(&ns) { - throw_range_error(&format!( - "Value {ns} out of range for Intl options property numberingSystem" - )); + let opt_ns = match get_option_string_coerced(current_options(), "numberingSystem") { + Some(ns) => { + let lower = ns.to_ascii_lowercase(); + if !is_well_formed_numbering_system(&lower) { + throw_range_error(&format!( + "Value {ns} out of range for Intl options property numberingSystem" + )); + } + Some(lower) } - } - let style = enum_option_strict(options, "style", &["long", "short", "narrow"], "long"); - let numeric = enum_option_strict(options, "numeric", &["always", "auto"], "always"); - set_internal_field(obj, KEY_RTF_STYLE, string_value(&style)); - set_internal_field(obj, KEY_NUMERIC, string_value(&numeric)); - install_bound_instance_function(obj, "format", rtf_bound_format_thunk as *const u8, 2); - install_bound_instance_function( - obj, + None => None, + }; + let (resolved_locale, numbering) = resolve_numbering_system(&locale, opt_ns.as_deref()); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_LOCALE, + string_value(&resolved_locale), + ); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_RTF_NUMBERING, + string_value(&numbering), + ); + let style = enum_option_strict( + current_options(), + "style", + &["long", "short", "narrow"], + "long", + ); + let numeric = + enum_option_strict(current_options(), "numeric", &["always", "auto"], "always"); + set_internal_field_from_raw_handle(&obj_handle, KEY_RTF_STYLE, string_value(&style)); + set_internal_field_from_raw_handle(&obj_handle, KEY_NUMERIC, string_value(&numeric)); + install_bound_instance_function_from_handle( + &obj_handle, + "format", + rtf_bound_format_thunk as *const u8, + 2, + ); + install_bound_instance_function_from_handle( + &obj_handle, "formatToParts", rtf_bound_to_parts_thunk as *const u8, 2, ); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle( + &obj_handle, "resolvedOptions", rtf_bound_resolved_options_thunk as *const u8, 0, ); } KIND_PLURAL_RULES => { - // `? GetOptionsObject(options)`, then GetOption in the exact order - // constructor-option-read-order.js asserts: localeMatcher, type, - // notation, compactDisplay, then SetNumberFormatDigitOptions - // (minimumIntegerDigits, min/maxFractionDigits, min/maxSignificantDigits, - // roundingIncrement, roundingMode, roundingPriority, trailingZeroDisplay). - let options = get_options_object(options); - let _ = enum_option_strict( - options, - "localeMatcher", - &["lookup", "best fit"], - "best fit", - ); - let pr_type = enum_option_strict(options, "type", &["cardinal", "ordinal"], "cardinal"); - set_internal_field(obj, KEY_TYPE, string_value(&pr_type)); - let notation = enum_option_strict( - options, - "notation", - &["standard", "scientific", "engineering", "compact"], - "standard", - ); - let compact_display = - enum_option_strict(options, "compactDisplay", &["short", "long"], "short"); - set_internal_field(obj, KEY_PR_NOTATION, string_value(¬ation)); - if notation == "compact" { - set_internal_field(obj, KEY_PR_COMPACT_DISPLAY, string_value(&compact_display)); - } - let min_int = get_option_number(options, "minimumIntegerDigits").unwrap_or(1.0); - set_internal_field(obj, KEY_PR_MIN_INT, min_int); - let min_frac_read = get_option_number(options, "minimumFractionDigits"); - let max_frac_read = get_option_number(options, "maximumFractionDigits"); - let min_sig = get_option_number(options, "minimumSignificantDigits"); - let max_sig = get_option_number(options, "maximumSignificantDigits"); - // Trailing SetNumberFormatDigitOptions reads — observed for read-order - // parity even though Perry's plural selection ignores their values. - let _ = get_option_value(options, "roundingIncrement"); - let _ = get_option_value(options, "roundingMode"); - let _ = get_option_value(options, "roundingPriority"); - let _ = get_option_value(options, "trailingZeroDisplay"); - if min_sig.is_some() || max_sig.is_some() { - set_internal_field(obj, KEY_PR_USE_SIG, bool_value(true)); - set_internal_field(obj, KEY_PR_MIN_SIG, min_sig.unwrap_or(1.0)); - set_internal_field(obj, KEY_PR_MAX_SIG, max_sig.unwrap_or(21.0)); - } else { - set_internal_field(obj, KEY_PR_USE_SIG, bool_value(false)); - // Reuse the values read above (in spec order) — re-reading would - // double-invoke the option getters and break read-order parity. - let min_frac = min_frac_read.unwrap_or(0.0); - let max_frac = max_frac_read.unwrap_or_else(|| min_frac.max(3.0)); - set_internal_field(obj, KEY_PR_MIN_FRAC, min_frac); - set_internal_field(obj, KEY_PR_MAX_FRAC, max_frac); - } - install_bound_instance_function( - obj, - "select", - plural_rules_bound_select_thunk as *const u8, - 1, - ); - install_bound_instance_function( - obj, - "selectRange", - plural_rules_bound_select_range_thunk as *const u8, - 2, - ); - install_bound_instance_function( - obj, - "resolvedOptions", - plural_rules_bound_resolved_options_thunk as *const u8, - 0, - ); + let obj = obj_handle.with_mut_ptr(|obj| { + list_relative_plural::configure_plural_rules(obj, &options_handle) + }); + obj_handle.set_raw_mut_ptr(obj); + } + KIND_DURATION_FORMAT => { + obj_handle.with_mut_ptr(|obj| duration_format::configure(obj, current_options())) + } + KIND_DISPLAY_NAMES => { + obj_handle.with_mut_ptr(|obj| display_names::configure(obj, current_options())) } - KIND_DURATION_FORMAT => duration_format::configure(obj, options), - KIND_DISPLAY_NAMES => display_names::configure(obj, options), _ => {} } - let proto = constructor_target_prototype(closure); + let proto = closure_handle.with_const_ptr(constructor_target_prototype); if JSValue::from_bits(proto.to_bits()).is_pointer() { - crate::object::prototype_chain::object_set_static_prototype(obj as usize, proto.to_bits()); - } - let instance = js_nanbox_pointer(obj as i64); + obj_handle.with_mut_ptr(|obj: *mut ObjectHeader| { + crate::object::prototype_chain::object_set_static_prototype( + obj as usize, + proto.to_bits(), + ) + }); + } + let instance = obj_handle.with_mut_ptr(|obj: *mut ObjectHeader| js_nanbox_pointer(obj as i64)); // ChainNumberFormat / ChainDateTimeFormat only (see `chain_legacy_constructed`): // Intl.Collator ignores its this-value, so it is deliberately excluded. if matches!(kind, KIND_NUMBER | KIND_DATE_TIME) { - if let Some(this_value) = ctor_guard::chain_legacy_constructed(closure, instance) { + if let Some(this_value) = closure_handle + .with_const_ptr(|closure| ctor_guard::chain_legacy_constructed(closure, instance)) + { return this_value; } } instance } -fn install_bound_instance_function( - obj: *mut ObjectHeader, - name: &str, - func_ptr: *const u8, - arity: u32, -) -> *mut ClosureHeader { - let closure = crate::closure::js_closure_alloc(func_ptr, 1); - if closure.is_null() { - return closure; - } - crate::closure::js_register_closure_arity(func_ptr, arity); - crate::closure::js_closure_set_capture_f64(closure, 0, js_nanbox_pointer(obj as i64)); - crate::object::set_bound_native_closure_name(closure, name); - crate::object::set_builtin_closure_length(closure as usize, arity); - // A bound Intl instance method (`nf.format`, `nf.resolvedOptions`, …) is a - // built-in non-constructor function: it has NO `[[Construct]]` and therefore - // no own `prototype` property (ECMA-262 §17 — built-in functions that aren't - // constructors don't get the auto-created `.prototype`). Flag it so - // `function_would_have_own_prototype` / the `new` path treat it like any - // other builtin (`Math.max`), matching `format-function-builtin.js`. - crate::object::set_builtin_closure_non_constructable(closure as usize); - 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), - ); - set_field(obj, name, js_nanbox_pointer(closure as i64)); - set_builtin_attrs(obj, name, PropertyAttrs::new(true, false, true)); - closure -} - pub(super) extern "C" fn number_format_constructor_thunk( closure: *const ClosureHeader, rest: f64, @@ -1696,48 +1748,6 @@ extern "C" fn supported_locales_of_thunk(_closure: *const ClosureHeader, rest: f supported_locales_array(rest_arg(rest, 0), rest_arg(rest, 1)) } -fn install_function( - owner: *mut ObjectHeader, - name: &str, - func_ptr: *const u8, - call_arity: u32, - length: u32, - has_rest: bool, -) -> f64 { - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return undefined(); - } - if has_rest { - crate::closure::js_register_closure_rest(func_ptr, call_arity); - } else { - crate::closure::js_register_closure_arity(func_ptr, call_arity); - } - crate::object::set_bound_native_closure_name(closure, name); - crate::object::set_builtin_closure_length(closure as usize, length); - // Intl prototype methods (`formatToParts`, `resolvedOptions`, …), the static - // `supportedLocalesOf`, and the this-based instance methods - // (`formatRange`/`formatRangeToParts`) installed through here are all - // built-in non-constructor functions: no `[[Construct]]`, hence no own - // `prototype` property (`builtin.js` asserts `hasOwnProperty("prototype")` - // is false and `isConstructor` is false). Flag them like any other builtin. - crate::object::set_builtin_closure_non_constructable(closure as usize); - 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 value = js_nanbox_pointer(closure as i64); - set_field(owner, name, value); - set_builtin_attrs(owner, name, PropertyAttrs::new(true, false, true)); - value -} - /// Set `proto[Symbol.toStringTag]` to `tag` (non-writable, non-enumerable, /// configurable) so `Object.prototype.toString.call(instance)` yields /// `[object ]` — the ECMA-402 default for every `Intl.*` prototype. diff --git a/crates/perry-runtime/src/intl/canon_aliases.rs b/crates/perry-runtime/src/intl/canon_aliases.rs index 21d65f3a98..ed5fa484a7 100644 --- a/crates/perry-runtime/src/intl/canon_aliases.rs +++ b/crates/perry-runtime/src/intl/canon_aliases.rs @@ -11,10 +11,11 @@ //! unicode-ext-canonicalize-*`). //! //! We keep this to the curated, code-unit-`|uvalue|`-shaped deprecated type -//! aliases that test262 exercises for the `ca` / `ks` / `ms` / `rg` / `sd` / -//! `tz` keys. Only the exact single-value replacements are represented; the -//! multi-territory / likely-subtag-dependent territoryAlias logic is out of -//! scope here (it lives in the language/region path, not the `-u-` extension). +//! aliases that test262 exercises for the transformed `m0` key and the Unicode +//! `ca` / `ks` / `ms` / `rg` / `sd` / `tz` keys. Only the exact single-value +//! replacements are represented; the multi-territory / likely-subtag-dependent +//! territoryAlias logic is out of scope here (it lives in the language/region +//! path, not the extension type-value path). /// `(key, deprecated_value, canonical_value)` for the Unicode-extension type /// aliases test262 canonicalizes. `deprecated_value` is the full `-`-joined @@ -60,6 +61,70 @@ fn u_ext_type_alias(key: &str, value: &str) -> Option<&'static str> { }) } +/// Rewrite the deprecated transformed-extension `m0-names` type. CLDR maps it +/// to `m0-prprname`, but ICU4X 2.2 leaves it unchanged. A transformed key is an +/// ASCII letter followed by a digit; that distinction prevents a two-letter +/// region in the optional `tlang` prefix from being mistaken for a field key. +fn canonicalize_transformed_extension_types(tag: &str) -> String { + let subtags: Vec<&str> = tag.split('-').collect(); + let Some(t_start) = subtags.iter().position(|s| s.eq_ignore_ascii_case("t")) else { + return tag.to_string(); + }; + if subtags[..t_start] + .iter() + .any(|s| s.eq_ignore_ascii_case("x")) + { + return tag.to_string(); + } + let t_end = subtags + .iter() + .enumerate() + .skip(t_start + 1) + .find_map(|(i, s)| (s.len() == 1).then_some(i)) + .unwrap_or(subtags.len()); + let is_tkey = |s: &str| { + let bytes = s.as_bytes(); + bytes.len() == 2 && bytes[0].is_ascii_alphabetic() && bytes[1].is_ascii_digit() + }; + let mut out: Vec = Vec::with_capacity(subtags.len()); + out.extend(subtags[..=t_start].iter().map(|s| s.to_string())); + let mut changed = false; + let mut i = t_start + 1; + while i < t_end { + if !is_tkey(subtags[i]) { + out.push(subtags[i].to_string()); + i += 1; + continue; + } + let key = subtags[i]; + out.push(key.to_string()); + let value_start = i + 1; + let value_end = (value_start..t_end) + .find(|&j| is_tkey(subtags[j])) + .unwrap_or(t_end); + if key.eq_ignore_ascii_case("m0") + && value_end == value_start + 1 + && subtags[value_start].eq_ignore_ascii_case("names") + { + out.push("prprname".to_string()); + changed = true; + } else { + out.extend( + subtags[value_start..value_end] + .iter() + .map(|s| s.to_string()), + ); + } + i = value_end; + } + out.extend(subtags[t_end..].iter().map(|s| s.to_string())); + if changed { + out.join("-") + } else { + tag.to_string() + } +} + /// Rewrite deprecated CLDR type values inside the Unicode (`-u-`) extension of /// an already-`normalize`d BCP-47 tag. Returns the tag unchanged when it has no /// `-u-` extension or no aliased value. @@ -70,6 +135,8 @@ fn u_ext_type_alias(key: &str, value: &str) -> Option<&'static str> { /// run that follows it (its `-`-joined 3..8-char subtags) is what we match and /// replace. pub(super) fn canonicalize_unicode_extension_types(tag: &str) -> String { + let transformed = canonicalize_transformed_extension_types(tag); + let tag = transformed.as_str(); let subtags: Vec<&str> = tag.split('-').collect(); // Find the `u` singleton (not inside a private-use `x` sequence). let mut u_start = None; @@ -132,3 +199,31 @@ pub(super) fn canonicalize_unicode_extension_types(tag: &str) -> String { tag.to_string() } } + +#[cfg(test)] +mod tests { + use super::canonicalize_unicode_extension_types; + + #[test] + fn canonicalizes_transformed_type_without_mistaking_region_for_key() { + assert_eq!( + canonicalize_unicode_extension_types("und-Latn-t-und-hani-m0-names"), + "und-Latn-t-und-hani-m0-prprname" + ); + assert_eq!( + canonicalize_unicode_extension_types("en-t-en-US-m0-names"), + "en-t-en-US-m0-prprname" + ); + assert_eq!( + canonicalize_unicode_extension_types("en-T-en-US-M0-NAMES"), + "en-T-en-US-M0-prprname" + ); + assert_eq!( + canonicalize_unicode_extension_types("en-t-en-m0-names-u-ca-gregory"), + "en-t-en-m0-prprname-u-ca-gregory" + ); + for unchanged in ["en-x-t-m0-names", "en-t-en-h0-hybrid"] { + assert_eq!(canonicalize_unicode_extension_types(unchanged), unchanged); + } + } +} diff --git a/crates/perry-runtime/src/intl/date_collator.rs b/crates/perry-runtime/src/intl/date_collator.rs index a2e3cfab78..6fd39575ee 100644 --- a/crates/perry-runtime/src/intl/date_collator.rs +++ b/crates/perry-runtime/src/intl/date_collator.rs @@ -5,6 +5,9 @@ use crate::closure::ClosureHeader; use crate::object::{js_object_alloc, ObjectHeader}; use crate::value::{js_nanbox_pointer, JSValue}; +mod compare; +use compare::{collator_compare_order, CollatorCompareOptions}; + /// ECMA-402 FormatDateTime / HandleDateTimeValue step 1: coerce the /// `format`/`formatToParts` argument to a TimeClip'd integer-millisecond value. /// `undefined` means "now". Every other value goes through ToNumber — a Date @@ -1603,22 +1606,123 @@ fn collation_normalize(s: &str) -> String { s.to_string() } -pub(crate) fn compare_strings(locale: &str, left: &str, right: &str) -> f64 { - let left = collation_normalize(left); - let right = collation_normalize(right); - let (left, right) = (left.as_str(), right.as_str()); - let ordering = if locale == "sv" || locale.starts_with("sv-") { - swedish_collation_key(left).cmp(&swedish_collation_key(right)) +fn locale_base_name(locale: &str) -> String { + locale + .split('-') + .take_while(|part| part.len() != 1) + .collect::>() + .join("-") +} + +fn supports_collation(locale: &str, collation: &str) -> bool { + collation == "eor" || (collation == "phonebk" && (locale == "de" || locale.starts_with("de-"))) +} + +/// Resolve Collator's relevant Unicode extension keys. Unsupported/irrelevant +/// keys and attributes are removed from the resolved locale; an explicit +/// supported option overrides the extension, while an unsupported option does +/// not displace a supported extension value. +pub(crate) fn resolve_collator_locale( + requested: &str, + collation_option: Option, + numeric_option: Option, + case_first_option: Option, +) -> (String, String, bool, String) { + let base = locale_base_name(requested); + let ext_collation = + unicode_extension_keyword(requested, "co").filter(|value| supports_collation(&base, value)); + let effective_collation = collation_option + .filter(|value| supports_collation(&base, value)) + .or_else(|| ext_collation.clone()) + .unwrap_or_else(|| "default".to_string()); + + let ext_numeric = + unicode_extension_keyword(requested, "kn").and_then(|value| match value.as_str() { + "" | "true" => Some(true), + "false" => Some(false), + _ => None, + }); + let effective_numeric = numeric_option.or(ext_numeric).unwrap_or(false); + let ext_case_first = unicode_extension_keyword(requested, "kf") + .filter(|value| ["upper", "lower", "false"].contains(&value.as_str())); + let effective_case_first = case_first_option + .clone() + .or_else(|| ext_case_first.clone()) + .unwrap_or_else(|| "false".to_string()); + + let mut keywords: Vec<(&str, String)> = Vec::new(); + if ext_collation.as_deref() == Some(effective_collation.as_str()) { + keywords.push(("co", effective_collation.clone())); + } + if ext_case_first.as_deref() == Some(effective_case_first.as_str()) { + keywords.push(("kf", effective_case_first.clone())); + } + if ext_numeric == Some(effective_numeric) { + keywords.push(( + "kn", + if effective_numeric { "" } else { "false" }.to_string(), + )); + } + let resolved_locale = if keywords.is_empty() { + base } else { - left.cmp(right) + let mut locale = format!("{base}-u"); + for (key, value) in keywords { + locale.push('-'); + locale.push_str(key); + if !value.is_empty() { + locale.push('-'); + locale.push_str(&value); + } + } + locale }; - match ordering { - std::cmp::Ordering::Less => -1.0, - std::cmp::Ordering::Equal => 0.0, - std::cmp::Ordering::Greater => 1.0, + ( + resolved_locale, + effective_collation, + effective_numeric, + effective_case_first, + ) +} + +#[cfg(feature = "string-normalize")] +fn base_collation_key(s: &str, preserve_case: bool) -> String { + use unicode_normalization::{char::is_combining_mark, UnicodeNormalization}; + s.nfd() + .filter(|ch| !is_combining_mark(*ch)) + .flat_map(|ch| { + if preserve_case { + ch.to_string().chars().collect::>() + } else { + ch.to_lowercase().collect::>() + } + }) + .collect() +} + +#[cfg(not(feature = "string-normalize"))] +fn base_collation_key(s: &str, preserve_case: bool) -> String { + if preserve_case { + s.to_string() + } else { + s.to_lowercase() } } +fn german_phonebook_key(s: &str) -> String { + let mut out = String::new(); + for ch in collation_normalize(s).chars() { + match ch.to_lowercase().next().unwrap_or(ch) { + 'ä' => out.push_str("ae"), + 'ö' => out.push_str("oe"), + 'ü' => out.push_str("ue"), + 'ß' => out.push_str("ss"), + other => out.push_str(&base_collation_key(&other.to_string(), false)), + } + } + out +} + /// `GetOption(options, key, "string", allowed, undefined)` for a Collator string /// option: only `undefined` selects the default (absent); every other value — /// `null` included — is coerced via `ToString` and rejected with a RangeError @@ -1710,14 +1814,42 @@ fn is_punctuation(c: char) -> bool { } pub(crate) fn collator_compare_object(obj: *const ObjectHeader, left: f64, right: f64) -> f64 { - let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); - let ignore_punct = get_field(obj, KEY_COL_IGNORE_PUNCT).to_bits() == crate::value::TAG_TRUE; - let (mut l, mut r) = (value_to_string(left), value_to_string(right)); - if ignore_punct { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_const_ptr(obj); + let left = scope.root_nanbox_f64(left); + let right = scope.root_nanbox_f64(right); + + // Snapshot every immutable collator slot before ToString can invoke user + // code and move the instance. Re-read the rooted object for every access. + let options = CollatorCompareOptions { + locale: get_string_field_from_raw_handle(&obj, KEY_LOCALE) + .unwrap_or_else(|| "en-US".to_string()), + usage: get_string_field_from_raw_handle(&obj, KEY_COL_USAGE) + .unwrap_or_else(|| "sort".to_string()), + collation: get_string_field_from_raw_handle(&obj, KEY_COL_COLLATION) + .unwrap_or_else(|| "default".to_string()), + sensitivity: get_string_field_from_raw_handle(&obj, KEY_COL_SENSITIVITY) + .unwrap_or_else(|| "variant".to_string()), + numeric: get_field_from_raw_handle(&obj, KEY_COL_NUMERIC).to_bits() + == crate::value::TAG_TRUE, + case_first: get_string_field_from_raw_handle(&obj, KEY_COL_CASE_FIRST) + .unwrap_or_else(|| "false".to_string()), + ignore_punctuation: get_field_from_raw_handle(&obj, KEY_COL_IGNORE_PUNCT).to_bits() + == crate::value::TAG_TRUE, + }; + let (mut l, mut r) = ( + value_to_string(left.get_nanbox_f64()), + value_to_string(right.get_nanbox_f64()), + ); + if options.ignore_punctuation { l = strip_ignorable_punctuation(&l); r = strip_ignorable_punctuation(&r); } - compare_strings(&locale, &l, &r) + match collator_compare_order(&options, &l, &r) { + std::cmp::Ordering::Less => -1.0, + std::cmp::Ordering::Equal => 0.0, + std::cmp::Ordering::Greater => 1.0, + } } pub(crate) extern "C" fn collator_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { @@ -1779,3 +1911,48 @@ pub(crate) fn collator_resolved_options_object(obj: *const ObjectHeader) -> f64 ); js_nanbox_pointer(out as i64) } + +#[cfg(test)] +mod collator_compare_tests { + use super::*; + + fn options(numeric: bool, case_first: &str) -> CollatorCompareOptions { + CollatorCompareOptions { + locale: "en".to_string(), + usage: "sort".to_string(), + collation: "default".to_string(), + sensitivity: "variant".to_string(), + numeric, + case_first: case_first.to_string(), + ignore_punctuation: false, + } + } + + #[test] + fn numeric_option_compares_digit_runs_by_value() { + assert_eq!( + collator_compare_order(&options(false, "false"), "10", "9"), + std::cmp::Ordering::Less + ); + assert_eq!( + collator_compare_order(&options(true, "false"), "10", "9"), + std::cmp::Ordering::Greater + ); + assert_eq!( + collator_compare_order(&options(true, "false"), "2", "02"), + std::cmp::Ordering::Equal + ); + } + + #[test] + fn case_first_option_changes_case_tie_breaking() { + assert_eq!( + collator_compare_order(&options(false, "upper"), "A", "a"), + std::cmp::Ordering::Less + ); + assert_eq!( + collator_compare_order(&options(false, "lower"), "A", "a"), + std::cmp::Ordering::Greater + ); + } +} diff --git a/crates/perry-runtime/src/intl/date_collator/compare.rs b/crates/perry-runtime/src/intl/date_collator/compare.rs new file mode 100644 index 0000000000..86777b425f --- /dev/null +++ b/crates/perry-runtime/src/intl/date_collator/compare.rs @@ -0,0 +1,148 @@ +use super::*; + +pub(super) struct CollatorCompareOptions { + pub(super) locale: String, + pub(super) usage: String, + pub(super) collation: String, + pub(super) sensitivity: String, + pub(super) numeric: bool, + pub(super) case_first: String, + pub(super) ignore_punctuation: bool, +} + +fn compare_digit_runs(left: &str, right: &str) -> std::cmp::Ordering { + let (left, right) = (left.as_bytes(), right.as_bytes()); + let (mut li, mut ri) = (0usize, 0usize); + while li < left.len() && ri < right.len() { + if left[li].is_ascii_digit() && right[ri].is_ascii_digit() { + let left_end = (li..left.len()) + .find(|&i| !left[i].is_ascii_digit()) + .unwrap_or(left.len()); + let right_end = (ri..right.len()) + .find(|&i| !right[i].is_ascii_digit()) + .unwrap_or(right.len()); + let left_significant = (li..left_end) + .find(|&i| left[i] != b'0') + .unwrap_or(left_end); + let right_significant = (ri..right_end) + .find(|&i| right[i] != b'0') + .unwrap_or(right_end); + let length_order = (left_end - left_significant).cmp(&(right_end - right_significant)); + if length_order != std::cmp::Ordering::Equal { + return length_order; + } + let value_order = + left[left_significant..left_end].cmp(&right[right_significant..right_end]); + if value_order != std::cmp::Ordering::Equal { + return value_order; + } + li = left_end; + ri = right_end; + continue; + } + + let left_char = std::str::from_utf8(&left[li..]) + .expect("collation key is UTF-8") + .chars() + .next() + .expect("left key is not exhausted"); + let right_char = std::str::from_utf8(&right[ri..]) + .expect("collation key is UTF-8") + .chars() + .next() + .expect("right key is not exhausted"); + let order = left_char.cmp(&right_char); + if order != std::cmp::Ordering::Equal { + return order; + } + li += left_char.len_utf8(); + ri += right_char.len_utf8(); + } + (left.len() - li).cmp(&(right.len() - ri)) +} + +fn compare_collation_keys(left: &str, right: &str, numeric: bool) -> std::cmp::Ordering { + if numeric { + compare_digit_runs(left, right) + } else { + left.cmp(right) + } +} + +fn case_first_order(left: &str, right: &str, case_first: &str) -> std::cmp::Ordering { + if !matches!(case_first, "upper" | "lower") { + return std::cmp::Ordering::Equal; + } + for (left, right) in left.chars().zip(right.chars()) { + if left == right || left.to_lowercase().to_string() != right.to_lowercase().to_string() { + continue; + } + let left_upper = left.is_uppercase(); + let right_upper = right.is_uppercase(); + if left_upper != right_upper { + let upper_first = case_first == "upper"; + return if left_upper == upper_first { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + }; + } + } + std::cmp::Ordering::Equal +} + +pub(super) fn collator_compare_order( + options: &CollatorCompareOptions, + left: &str, + right: &str, +) -> std::cmp::Ordering { + let swedish = options.locale == "sv" || options.locale.starts_with("sv-"); + let phonebook = options.collation == "phonebk" + || (options.usage == "search" + && (options.locale == "de" || options.locale.starts_with("de-"))); + let primary_left = if swedish { + swedish_collation_key(left) + .into_iter() + .filter_map(char::from_u32) + .collect() + } else if phonebook { + german_phonebook_key(left) + } else { + base_collation_key(left, false) + }; + let primary_right = if swedish { + swedish_collation_key(right) + .into_iter() + .filter_map(char::from_u32) + .collect() + } else if phonebook { + german_phonebook_key(right) + } else { + base_collation_key(right, false) + }; + let primary = compare_collation_keys(&primary_left, &primary_right, options.numeric); + if primary != std::cmp::Ordering::Equal || options.sensitivity == "base" { + return primary; + } + let normalized_left = collation_normalize(left); + let normalized_right = collation_normalize(right); + if options.sensitivity == "accent" { + return compare_collation_keys( + &normalized_left.to_lowercase(), + &normalized_right.to_lowercase(), + options.numeric, + ); + } + let case_order = case_first_order(&normalized_left, &normalized_right, &options.case_first); + if case_order != std::cmp::Ordering::Equal { + return case_order; + } + if options.sensitivity == "case" { + return compare_collation_keys( + &base_collation_key(&normalized_left, true), + &base_collation_key(&normalized_right, true), + options.numeric, + ); + } + compare_collation_keys(&normalized_left, &normalized_right, options.numeric) +} diff --git a/crates/perry-runtime/src/intl/display_names.rs b/crates/perry-runtime/src/intl/display_names.rs index dedd2c467c..e3aa6f1b01 100644 --- a/crates/perry-runtime/src/intl/display_names.rs +++ b/crates/perry-runtime/src/intl/display_names.rs @@ -79,7 +79,7 @@ fn get_options_object(options: f64) -> f64 { if jv.is_undefined() { return options; } - if object_ptr_from_value(options).is_some() { + if crate::proxy::js_proxy_is_proxy(options) != 0 || object_ptr_from_value(options).is_some() { return options; } throw_type_error("Intl.DisplayNames: options must be an object"); @@ -88,18 +88,26 @@ fn get_options_object(options: f64) -> f64 { /// Configure a freshly-allocated `Intl.DisplayNames` instance: validate the /// options bag (in spec order) and install the bound instance methods. pub(super) fn configure(obj: *mut ObjectHeader, options_arg: f64) { - let options = get_options_object(options_arg); + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let options_handle = scope.root_nanbox_f64(get_options_object(options_arg)); + let current_options = || options_handle.get_nanbox_f64(); // localeMatcher, then style, then type (required), then fallback, then // languageDisplay — the order the resolvedOptions / option-* tests rely on. let _matcher = dn_enum_option( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", ); - let style = dn_enum_option(options, "style", &["narrow", "short", "long"], "long"); - let type_ = match dn_get_option_string(options, "type") { + let style = dn_enum_option( + current_options(), + "style", + &["narrow", "short", "long"], + "long", + ); + let type_ = match dn_get_option_string(current_options(), "type") { Some(v) => { if ![ "language", @@ -119,26 +127,30 @@ pub(super) fn configure(obj: *mut ObjectHeader, options_arg: f64) { } None => throw_type_error("Intl.DisplayNames: options.type is required"), }; - let fallback = dn_enum_option(options, "fallback", &["code", "none"], "code"); + let fallback = dn_enum_option(current_options(), "fallback", &["code", "none"], "code"); // languageDisplay is read + validated unconditionally, but only applies to — // and is reported by resolvedOptions for — `type: "language"`. let language_display = dn_enum_option( - options, + current_options(), "languageDisplay", &["dialect", "standard"], "dialect", ); - set_internal_field(obj, KEY_STYLE, string_value(&style)); - set_internal_field(obj, KEY_TYPE, string_value(&type_)); - set_internal_field(obj, KEY_DN_FALLBACK, string_value(&fallback)); + set_internal_field_from_raw_handle(&obj_handle, KEY_STYLE, string_value(&style)); + set_internal_field_from_raw_handle(&obj_handle, KEY_TYPE, string_value(&type_)); + set_internal_field_from_raw_handle(&obj_handle, KEY_DN_FALLBACK, string_value(&fallback)); if type_ == "language" { - set_internal_field(obj, KEY_DN_LANG_DISPLAY, string_value(&language_display)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_DN_LANG_DISPLAY, + string_value(&language_display), + ); } - install_bound_instance_function(obj, "of", bound_of_thunk as *const u8, 1); - install_bound_instance_function( - obj, + install_bound_instance_function_from_handle(&obj_handle, "of", bound_of_thunk as *const u8, 1); + install_bound_instance_function_from_handle( + &obj_handle, "resolvedOptions", bound_resolved_options_thunk as *const u8, 0, diff --git a/crates/perry-runtime/src/intl/duration_format.rs b/crates/perry-runtime/src/intl/duration_format.rs index 386cc08b25..c55e3eefa8 100644 --- a/crates/perry-runtime/src/intl/duration_format.rs +++ b/crates/perry-runtime/src/intl/duration_format.rs @@ -122,8 +122,11 @@ fn get_duration_unit_options( digital_base: &str, prev_style: Option<&str>, ) -> (String, String) { + let scope = crate::gc::RuntimeHandleScope::new(); + let options_handle = scope.root_nanbox_f64(options); + let current_options = || options_handle.get_nanbox_f64(); // 1. style = GetOption(options, unit, string, allowed, undefined) - let mut style = match df_get_option_string(options, unit) { + let mut style = match df_get_option_string(current_options(), unit) { Some(v) => { if !allowed.contains(&v.as_str()) { throw_range_error(&format!( @@ -163,7 +166,7 @@ fn get_duration_unit_options( } // 6. display = GetOption(options, unitDisplay, string, «auto,always», displayDefault) let display = df_enum_option( - options, + current_options(), &display_key_field(unit), &["auto", "always"], display_default, @@ -212,16 +215,20 @@ fn report_style(style: &str) -> &str { /// Configure a freshly-allocated `Intl.DurationFormat` instance: read + validate /// the options bag (in spec order). pub(super) fn configure(obj: *mut ObjectHeader, options: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let options_handle = scope.root_nanbox_f64(options); + let current_options = || options_handle.get_nanbox_f64(); // GetOptionsObject: `undefined` → empty options; any other non-object // (notably `null` and primitives) is a TypeError. Object-like values — // including arrays, functions, and Proxies (all pointer-tagged) — are // accepted, so a property-bag Proxy still has its traps observed. Symbols // are also pointer-tagged, so `is_pointer()` alone would wrongly admit them; // exclude registered symbols explicitly. - let opts_jv = JSValue::from_bits(options.to_bits()); + let opts_jv = JSValue::from_bits(current_options().to_bits()); let is_symbol = opts_jv.is_pointer() && crate::symbol::is_registered_symbol( - (options.to_bits() & crate::value::POINTER_MASK) as usize, + (current_options().to_bits() & crate::value::POINTER_MASK) as usize, ); if !opts_jv.is_undefined() && (!opts_jv.is_pointer() || is_symbol) { throw_type_error("Intl.DurationFormat: options must be an object"); @@ -230,13 +237,13 @@ pub(super) fn configure(obj: *mut ObjectHeader, options: f64) { // Order (constructor-options-order): localeMatcher, numberingSystem, style, // then each unit + unitDisplay, then fractionalDigits. let _matcher = df_enum_option( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", ); - let opt_ns = match df_get_option_string(options, "numberingSystem") { + let opt_ns = match df_get_option_string(current_options(), "numberingSystem") { Some(ns) => { if !valid_numbering_system(&ns) { throw_range_error(&format!( @@ -250,42 +257,47 @@ pub(super) fn configure(obj: *mut ObjectHeader, options: f64) { // ResolveLocale for `nu`: reconcile the option with the requested locale's // `-u-nu-` keyword (stored in KEY_LOCALE at construction) and update both the // resolved locale and numbering system. - let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); + let locale = get_string_field_from_raw_handle(&obj_handle, KEY_LOCALE) + .unwrap_or_else(|| "en-US".to_string()); let (resolved_locale, numbering) = super::resolve_numbering_system(&locale, opt_ns.as_deref()); - set_internal_field(obj, KEY_LOCALE, string_value(&resolved_locale)); - set_internal_field(obj, KEY_DF_NUMBERING, string_value(&numbering)); + set_internal_field_from_raw_handle(&obj_handle, KEY_LOCALE, string_value(&resolved_locale)); + set_internal_field_from_raw_handle(&obj_handle, KEY_DF_NUMBERING, string_value(&numbering)); let base_style = df_enum_option( - options, + current_options(), "style", &["long", "short", "narrow", "digital"], "short", ); - set_internal_field(obj, KEY_DF_STYLE, string_value(&base_style)); + set_internal_field_from_raw_handle(&obj_handle, KEY_DF_STYLE, string_value(&base_style)); let mut prev_style: Option = None; for (unit, allowed, digital_base) in UNITS.iter().copied() { let (style, display) = get_duration_unit_options( - options, + current_options(), unit, allowed, &base_style, digital_base, prev_style.as_deref(), ); - set_internal_field(obj, &style_key(unit), string_value(report_style(&style))); - set_internal_field(obj, &display_key(unit), string_value(&display)); + set_internal_field_from_raw_handle( + &obj_handle, + &style_key(unit), + string_value(report_style(&style)), + ); + set_internal_field_from_raw_handle(&obj_handle, &display_key(unit), string_value(&display)); prev_style = Some(style); } // fractionalDigits: integer in [0, 9], else RangeError. Read last. - if let Some(n) = get_option_number(options, "fractionalDigits") { + if let Some(n) = get_option_number(current_options(), "fractionalDigits") { if !n.is_finite() || n.fract() != 0.0 || !(0.0..=9.0).contains(&n) { throw_range_error( "Value out of range for Intl.DurationFormat options property fractionalDigits", ); } - set_internal_field(obj, KEY_DF_FRACTIONAL, n); + set_internal_field_from_raw_handle(&obj_handle, KEY_DF_FRACTIONAL, n); } // Unlike `Intl.NumberFormat.prototype.format` (a bound getter), the @@ -294,17 +306,24 @@ pub(super) fn configure(obj: *mut ObjectHeader, options: f64) { // We install them as own instance properties (Perry's method dispatch resolves // own properties) but back them with the implicit-`this` thunks, so a // detached call lands on an undefined receiver and `RequireInternalSlot` throws. - super::install_function(obj, "format", format_thunk as *const u8, 1, 1, false); - super::install_function( - obj, + super::install_function_from_handle( + &obj_handle, + "format", + format_thunk as *const u8, + 1, + 1, + false, + ); + super::install_function_from_handle( + &obj_handle, "formatToParts", to_parts_thunk as *const u8, 1, 1, false, ); - super::install_function( - obj, + super::install_function_from_handle( + &obj_handle, "resolvedOptions", resolved_options_thunk as *const u8, 0, diff --git a/crates/perry-runtime/src/intl/list_relative_plural.rs b/crates/perry-runtime/src/intl/list_relative_plural.rs index 3d1247d93a..935f12ed50 100644 --- a/crates/perry-runtime/src/intl/list_relative_plural.rs +++ b/crates/perry-runtime/src/intl/list_relative_plural.rs @@ -342,46 +342,161 @@ fn rtf_auto_word(value: f64, unit: &str, style: &str) -> Option<&'static str> { } } -/// Build en-US relative-time parts for `value` in `unit`. -/// -/// When `numeric == "auto"`, substitutes the CLDR relative word form -/// (`"yesterday"` / `"today"` / `"tomorrow"`, `"last/this/next "`, -/// `"now"`, …) for the discrete integer values CLDR names; otherwise (and for -/// every other value) renders the long numeric form (`"in 2 days"` / -/// `"1 day ago"`). `format` and `formatToParts` share this path so they stay -/// consistent. A word-form result is a single `"literal"` part (no unit field), -/// matching Node / ECMA-402 FormatRelativeTimeToParts. -/// -/// `style` is consulted only for the auto word forms (`week`/`month`/ -/// `quarter`/`year` short abbreviations); the numeric path still uses the long -/// unit names (a pre-existing limitation of the en-US fallback formatter). +fn intl_language(locale: &str) -> &str { + locale.split(['-', '_']).next().unwrap_or(locale) +} + +fn polish_plural(value: f64) -> &'static str { + let abs = value.abs(); + if abs.fract() != 0.0 { + return "other"; + } + let i = abs as u64; + if i == 1 { + "one" + } else if matches!(i % 10, 2..=4) && !matches!(i % 100, 12..=14) { + "few" + } else { + "many" + } +} + +fn polish_unit(unit: &str, style: &str, category: &str) -> &'static str { + if style != "long" { + return match (style, unit, category) { + ("narrow", "second", _) => "s", + (_, "second", _) => "sek.", + (_, "minute", _) => "min", + ("narrow", "hour", _) => "g.", + (_, "hour", _) => "godz.", + (_, "day", "one") => "dzień", + (_, "day", "other") => "dnia", + (_, "day", _) => "dni", + (_, "week", "one") => "tydz.", + (_, "week", "other") => "tyg.", + (_, "week", _) => "tyg.", + (_, "month", _) => "mies.", + (_, "quarter", _) => "kw.", + (_, "year", "one") => "rok", + (_, "year", "few") => "lata", + (_, "year", "other") => "roku", + (_, "year", _) => "lat", + _ => "", + }; + } + match (unit, category) { + ("second", "one") => "sekundę", + ("second", "few" | "other") => "sekundy", + ("second", _) => "sekund", + ("minute", "one") => "minutę", + ("minute", "few" | "other") => "minuty", + ("minute", _) => "minut", + ("hour", "one") => "godzinę", + ("hour", "few" | "other") => "godziny", + ("hour", _) => "godzin", + ("day", "one") => "dzień", + ("day", "other") => "dnia", + ("day", _) => "dni", + ("week", "one") => "tydzień", + ("week", "few") => "tygodnie", + ("week", "other") => "tygodnia", + ("week", _) => "tygodni", + ("month", "one") => "miesiąc", + ("month", "few") => "miesiące", + ("month", "other") => "miesiąca", + ("month", _) => "miesięcy", + ("quarter", "one") => "kwartał", + ("quarter", "few") => "kwartały", + ("quarter", "other") => "kwartału", + ("quarter", _) => "kwartałów", + ("year", "one") => "rok", + ("year", "few") => "lata", + ("year", "other") => "roku", + ("year", _) => "lat", + _ => "", + } +} + +fn english_unit(unit: &str, style: &str, singular: bool) -> String { + if style == "long" { + return if singular { + unit.to_string() + } else { + format!("{unit}s") + }; + } + match (unit, singular) { + ("second", _) => "sec.".to_string(), + ("minute", _) => "min.".to_string(), + ("hour", _) => "hr.".to_string(), + ("day", true) => "day".to_string(), + ("day", false) => "days".to_string(), + ("week", _) => "wk.".to_string(), + ("month", _) => "mo.".to_string(), + ("quarter", true) => "qtr.".to_string(), + ("quarter", false) => "qtrs.".to_string(), + ("year", _) => "yr.".to_string(), + _ => unit.to_string(), + } +} + +/// Build relative-time parts around the shared NumberFormat rendering core. +/// This keeps digit substitution, grouping, separators, and typed number parts +/// identical to `new Intl.NumberFormat(locale).formatToParts(value)`. pub(crate) fn rtf_parts( value: f64, unit: &str, numeric: &str, style: &str, + locale: &str, + numbering_system: &str, ) -> Vec<(&'static str, String)> { - if numeric == "auto" { + let language = intl_language(locale); + if language == "en" && numeric == "auto" { if let Some(word) = rtf_auto_word(value, unit, style) { return vec![("literal", word.to_string())]; } } let abs = value.abs(); - let num_str = format_number_parts(abs, "en-US", None, None); - let unit_display = if abs == 1.0 { - unit.to_string() + let mut resolved = nf_resolved_default(locale); + resolved.numbering_system = numbering_system.to_string(); + if language == "pl" { + resolved.use_grouping = "min2".to_string(); + } + let number_parts = number_parts_from_resolved(&resolved, abs); + let unit_display = if language == "pl" { + polish_unit(unit, style, polish_plural(abs)).to_string() } else { - format!("{unit}s") + english_unit(unit, style, abs == 1.0) }; let past = value.is_sign_negative(); let mut parts: Vec<(&'static str, String)> = Vec::new(); - if past { - split_numeric_parts(&num_str, "en-US", &mut parts); - parts.push(("literal", format!(" {unit_display} ago"))); + if language == "pl" { + if !past { + parts.push(("literal", "za ".to_string())); + } + parts.extend(number_parts); + parts.push(( + "literal", + if past { + format!(" {unit_display} temu") + } else { + format!(" {unit_display}") + }, + )); } else { - parts.push(("literal", "in ".to_string())); - split_numeric_parts(&num_str, "en-US", &mut parts); - parts.push(("literal", format!(" {unit_display}"))); + if !past { + parts.push(("literal", "in ".to_string())); + } + parts.extend(number_parts); + parts.push(( + "literal", + if past { + format!(" {unit_display} ago") + } else { + format!(" {unit_display}") + }, + )); } parts } @@ -417,6 +532,9 @@ pub(crate) fn rtf_instance_parts_and_unit( // being held as a raw pointer across a GC-capable call (#6960 / CodeRabbit). let numeric = get_string_field(obj, KEY_NUMERIC).unwrap_or_else(|| "always".to_string()); let style = get_string_field(obj, KEY_RTF_STYLE).unwrap_or_else(|| "long".to_string()); + let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); + let numbering_system = + get_string_field(obj, KEY_RTF_NUMBERING).unwrap_or_else(|| "latn".to_string()); // ToNumber: a Symbol/BigInt value throws TypeError *before* the finite-ness // RangeError (format/value-symbol.js); an object's valueOf is invoked. let number = to_number_reject_bigint(value); @@ -434,7 +552,10 @@ pub(crate) fn rtf_instance_parts_and_unit( "Value {unit_str} out of range for Intl.RelativeTimeFormat.format() unit" )); }; - (rtf_parts(number, unit, &numeric, &style), unit) + ( + rtf_parts(number, unit, &numeric, &style, &locale, &numbering_system), + unit, + ) } pub(crate) fn rtf_instance_parts( @@ -527,7 +648,13 @@ pub(crate) fn rtf_resolved_options_object(obj: *const ObjectHeader) -> f64 { "numeric", string_value(&get_string_field(obj, KEY_NUMERIC).unwrap_or_else(|| "always".to_string())), ); - set_field(out, "numberingSystem", string_value("latn")); + set_field( + out, + "numberingSystem", + string_value( + &get_string_field(obj, KEY_RTF_NUMBERING).unwrap_or_else(|| "latn".to_string()), + ), + ); js_nanbox_pointer(out as i64) } @@ -543,6 +670,156 @@ pub(crate) extern "C" fn rtf_bound_resolved_options_thunk(closure: *const Closur // ---- Intl.PluralRules ------------------------------------------------------ +fn plural_digit_integer(number: f64, min: f64, max: f64) -> Option { + (number.is_finite() && number >= min && number <= max).then(|| number.floor()) +} + +fn plural_digit_option(options: f64, key: &str, min: f64, max: f64) -> Option { + let value = get_option_value(options, key); + if JSValue::from_bits(value.to_bits()).is_undefined() { + return None; + } + let number = to_number_reject_bigint(value); + let Some(integer) = plural_digit_integer(number, min, max) else { + throw_range_error(&format!( + "Value {number} out of range for Intl.PluralRules options property {key}" + )); + }; + Some(integer) +} + +pub(super) fn configure_plural_rules( + obj: *mut ObjectHeader, + options_handle: &crate::gc::RuntimeHandle<'_>, +) -> *mut ObjectHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + // `? GetOptionsObject(options)`, then GetOption in the exact order asserted + // by constructor-option-read-order.js. Every read reloads the rooted Proxy: + // its getter appends to a JS array and can therefore move both arguments and + // the partially initialized result during GC. + let _ = get_options_object(options_handle.get_nanbox_f64()); + let _ = enum_option_strict( + options_handle.get_nanbox_f64(), + "localeMatcher", + &["lookup", "best fit"], + "best fit", + ); + let pr_type = enum_option_strict( + options_handle.get_nanbox_f64(), + "type", + &["cardinal", "ordinal"], + "cardinal", + ); + set_internal_field_from_raw_handle(&obj_handle, KEY_TYPE, string_value(&pr_type)); + let notation = enum_option_strict( + options_handle.get_nanbox_f64(), + "notation", + &["standard", "scientific", "engineering", "compact"], + "standard", + ); + let compact_display = enum_option_strict( + options_handle.get_nanbox_f64(), + "compactDisplay", + &["short", "long"], + "short", + ); + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_NOTATION, string_value(¬ation)); + if notation == "compact" { + set_internal_field_from_raw_handle( + &obj_handle, + KEY_PR_COMPACT_DISPLAY, + string_value(&compact_display), + ); + } + let min_int = plural_digit_option( + options_handle.get_nanbox_f64(), + "minimumIntegerDigits", + 1.0, + 21.0, + ) + .unwrap_or(1.0); + let min_frac_read = plural_digit_option( + options_handle.get_nanbox_f64(), + "minimumFractionDigits", + 0.0, + 100.0, + ); + let max_frac_read = plural_digit_option( + options_handle.get_nanbox_f64(), + "maximumFractionDigits", + 0.0, + 100.0, + ); + let min_sig = plural_digit_option( + options_handle.get_nanbox_f64(), + "minimumSignificantDigits", + 1.0, + 21.0, + ); + let max_sig = plural_digit_option( + options_handle.get_nanbox_f64(), + "maximumSignificantDigits", + 1.0, + 21.0, + ); + let _ = get_option_value(options_handle.get_nanbox_f64(), "roundingIncrement"); + let _ = get_option_value(options_handle.get_nanbox_f64(), "roundingMode"); + let _ = get_option_value(options_handle.get_nanbox_f64(), "roundingPriority"); + let _ = get_option_value(options_handle.get_nanbox_f64(), "trailingZeroDisplay"); + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_MIN_INT, min_int); + if min_sig.is_some() || max_sig.is_some() { + let min_sig = min_sig.unwrap_or(1.0); + let max_sig = max_sig.unwrap_or(21.0); + if max_sig < min_sig { + throw_range_error( + "maximumSignificantDigits is below minimumSignificantDigits for Intl.PluralRules", + ); + } + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_USE_SIG, bool_value(true)); + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_MIN_SIG, min_sig); + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_MAX_SIG, max_sig); + } else { + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_USE_SIG, bool_value(false)); + let min_frac = min_frac_read.unwrap_or(0.0); + let max_frac = max_frac_read.unwrap_or_else(|| min_frac.max(3.0)); + if max_frac < min_frac { + throw_range_error( + "maximumFractionDigits is below minimumFractionDigits for Intl.PluralRules", + ); + } + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_MIN_FRAC, min_frac); + set_internal_field_from_raw_handle(&obj_handle, KEY_PR_MAX_FRAC, max_frac); + } + obj_handle.with_mut_ptr(|obj| { + install_bound_instance_function( + obj, + "select", + plural_rules_bound_select_thunk as *const u8, + 1, + ) + }); + obj_handle.with_mut_ptr(|obj| { + install_function( + obj, + "selectRange", + plural_rules_select_range_thunk as *const u8, + 2, + 2, + false, + ) + }); + obj_handle.with_mut_ptr(|obj| { + install_bound_instance_function( + obj, + "resolvedOptions", + plural_rules_bound_resolved_options_thunk as *const u8, + 0, + ) + }); + obj_handle.across_mut::(|| ()).1 +} + /// en plural-category selection. Cardinal: `i == 1 && v == 0` → "one". Ordinal /// (UTS #35 en ordinal rules): 1st→"one", 2nd→"two", 3rd→"few", else "other". pub(crate) fn plural_select_en(n: f64, is_ordinal: bool) -> &'static str { @@ -570,18 +847,140 @@ pub(crate) fn plural_select_en(n: f64, is_ordinal: bool) -> &'static str { } } -pub(crate) fn plural_categories(is_ordinal: bool) -> &'static [&'static str] { +pub(crate) fn plural_categories(locale: &str, is_ordinal: bool) -> &'static [&'static str] { if is_ordinal { - &["one", "two", "few", "other"] - } else { - &["one", "other"] + return if intl_language(locale) == "en" { + &["one", "two", "few", "other"] + } else { + &["other"] + }; + } + match intl_language(locale) { + "ar" => &["zero", "one", "two", "few", "many", "other"], + "fa" | "en" => &["one", "other"], + "fr" => &["one", "many", "other"], + "gv" => &["one", "two", "few", "many", "other"], + "ko" => &["other"], + "sl" => &["one", "two", "few", "other"], + _ => &["one", "other"], + } +} + +fn plural_category(language: &str, is_ordinal: bool, notation: &str, n: f64) -> &'static str { + if is_ordinal { + return if language == "en" { + plural_select_en(n, true) + } else { + "other" + }; + } + if !n.is_finite() { + return "other"; + } + let abs = n.abs(); + if language == "fr" { + return if abs < 2.0 { + "one" + } else if (notation == "compact" && abs >= 1_000_000.0) + || (abs.fract() == 0.0 && abs != 0.0 && abs % 1_000_000.0 == 0.0) + { + "many" + } else { + "other" + }; + } + let integer = abs.fract() == 0.0; + let i = abs as u64; + match language { + "ar" if abs == 0.0 => "zero", + "ar" if abs == 1.0 => "one", + "ar" if abs == 2.0 => "two", + "ar" if integer && matches!(i % 100, 3..=10) => "few", + "ar" if integer && matches!(i % 100, 11..=99) => "many", + "ar" => "other", + "fa" if i == 0 || abs == 1.0 => "one", + "fa" => "other", + "gv" if !integer => "many", + "gv" if i % 10 == 1 => "one", + "gv" if i % 10 == 2 => "two", + "gv" if matches!(i % 100, 0 | 20 | 40 | 60 | 80) => "few", + "gv" => "other", + "ko" => "other", + "sl" if integer && i % 100 == 1 => "one", + "sl" if integer && i % 100 == 2 => "two", + "sl" if !integer || matches!(i % 100, 3..=4) => "few", + "sl" => "other", + _ => plural_select_en(n, false), } } pub(crate) fn plural_rules_select(obj: *const ObjectHeader, value: f64) -> f64 { - let n = JSValue::from_bits(value.to_bits()).to_number(); - let is_ordinal = get_string_field(obj, KEY_TYPE).as_deref() == Some("ordinal"); - string_value(plural_select_en(n, is_ordinal)) + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_const_ptr(obj); + let value = scope.root_nanbox_f64(value); + let is_ordinal = get_string_field_from_raw_handle(&obj, KEY_TYPE).as_deref() == Some("ordinal"); + let locale = + get_string_field_from_raw_handle(&obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); + let notation = get_string_field_from_raw_handle(&obj, KEY_PR_NOTATION) + .unwrap_or_else(|| "standard".to_string()); + let n = to_number_reject_bigint(value.get_nanbox_f64()); + let language = intl_language(&locale); + string_value(plural_category(&language, is_ordinal, ¬ation, n)) +} + +#[cfg(test)] +mod plural_category_tests { + use super::*; + + #[test] + fn digit_options_validate_before_flooring() { + assert_eq!(plural_digit_integer(20.9, 1.0, 21.0), Some(20.0)); + assert_eq!(plural_digit_integer(21.9, 1.0, 21.0), None); + assert_eq!(plural_digit_integer(100.5, 0.0, 100.0), None); + assert_eq!(plural_digit_integer(-0.5, 0.0, 100.0), None); + assert_eq!(plural_digit_integer(f64::INFINITY, 0.0, 100.0), None); + } + + #[test] + fn locale_selectors_only_return_advertised_categories() { + let samples = [ + 0.0, + 0.5, + 1.0, + 2.0, + 3.0, + 4.0, + 5.0, + 10.0, + 11.0, + 20.0, + 21.0, + 100.0, + 1_000_000.0, + ]; + for locale in ["ar", "en", "fa", "fr", "gv", "ko", "sl"] { + for is_ordinal in [false, true] { + for value in samples { + let category = plural_category(locale, is_ordinal, "standard", value); + assert!( + plural_categories(locale, is_ordinal).contains(&category), + "{locale} ordinal={is_ordinal} value={value} returned {category}" + ); + } + } + } + } + + #[test] + fn locale_specific_cardinal_rules_cover_their_extra_categories() { + assert_eq!(plural_category("ar", false, "standard", 0.0), "zero"); + assert_eq!(plural_category("ar", false, "standard", 2.0), "two"); + assert_eq!(plural_category("ar", false, "standard", 7.0), "few"); + assert_eq!(plural_category("ar", false, "standard", 15.0), "many"); + assert_eq!(plural_category("gv", false, "standard", 1.5), "many"); + assert_eq!(plural_category("sl", false, "standard", 3.0), "few"); + assert_eq!(plural_category("ko", true, "standard", 1.0), "other"); + } } pub(crate) extern "C" fn plural_rules_select_thunk( @@ -609,15 +1008,6 @@ pub(crate) extern "C" fn plural_rules_select_range_thunk( plural_select_range(start, end) } -pub(crate) extern "C" fn plural_rules_bound_select_range_thunk( - closure: *const ClosureHeader, - start: f64, - end: f64, -) -> f64 { - let _obj = captured_intl_object(closure, "selectRange", KIND_PLURAL_RULES); - plural_select_range(start, end) -} - pub(crate) fn plural_select_range(start: f64, end: f64) -> f64 { // PluralRules.prototype.selectRange(start, end): a `undefined` endpoint is a // TypeError (step 3), evaluated *before* the `? ToNumber` coercions — and @@ -693,7 +1083,8 @@ pub(crate) fn plural_rules_resolved_options_object(obj: *const ObjectHeader) -> ); } let mut categories = js_array_alloc(0); - for cat in plural_categories(is_ordinal) { + let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); + for cat in plural_categories(&locale, is_ordinal) { categories = js_array_push_f64(categories, string_value(cat)); } set_field( diff --git a/crates/perry-runtime/src/intl/locale.rs b/crates/perry-runtime/src/intl/locale.rs index a49e8189ec..837d898a7a 100644 --- a/crates/perry-runtime/src/intl/locale.rs +++ b/crates/perry-runtime/src/intl/locale.rs @@ -317,27 +317,34 @@ fn base_name(p: &ParsedLocale) -> String { fn full_string(p: &ParsedLocale) -> String { let mut s = base_name(p); + let mut extensions = p.other_ext.clone(); if !p.attributes.is_empty() || !p.keywords.is_empty() { - s.push_str("-u"); + let mut unicode = String::new(); let mut attrs = p.attributes.clone(); attrs.sort(); for a in attrs { - s.push('-'); - s.push_str(&a); + if !unicode.is_empty() { + unicode.push('-'); + } + unicode.push_str(&a); } for (k, v) in &p.keywords { - s.push('-'); - s.push_str(k); + if !unicode.is_empty() { + unicode.push('-'); + } + unicode.push_str(k); if !v.is_empty() { - s.push('-'); - s.push_str(v); + unicode.push('-'); + unicode.push_str(v); } } + extensions.push(('u', unicode)); } - // Other extensions, sorted by singleton with private-use (`x`) last. - let mut others = p.other_ext.clone(); - others.sort_by_key(|(c, _)| if *c == 'x' { '{' } else { *c }); - for (c, content) in others { + // All extensions are sorted by singleton with private-use (`x`) last. The + // Unicode extension is not privileged: `en-a-bar-u-baz-x-private` keeps + // `a` before `u` (Locale/constructor-tag.js). + extensions.sort_by_key(|(c, _)| if *c == 'x' { '{' } else { *c }); + for (c, content) in extensions { s.push('-'); s.push(c); s.push('-'); @@ -669,7 +676,11 @@ pub(super) extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, throw_type_error("Intl.Locale: tag must be a String or an Intl.Locale instance"); }; - let Some(mut parsed) = parse_language_tag(&tag) else { + #[cfg(feature = "intl-locale")] + let canonical_tag = super::canonicalize_language_tag(&tag); + #[cfg(not(feature = "intl-locale"))] + let canonical_tag = Some(tag.clone()); + let Some(mut parsed) = canonical_tag.as_deref().and_then(parse_language_tag) else { throw_range_error(&format!("Incorrect locale information provided: {tag}")); }; canonicalize_aliases(&mut parsed); @@ -682,6 +693,18 @@ pub(super) extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, } } apply_options(&mut parsed, options); + // ApplyOptionsToTag canonicalizes both before and after the overrides. The + // second pass is observable for numeric region aliases (`554` -> `NZ`) and + // for replacements whose result depends on an overridden base subtag. + #[cfg(feature = "intl-locale")] + { + let adjusted = full_string(&parsed); + let canonical = super::canonicalize_language_tag(&adjusted) + .unwrap_or_else(|| throw_range_error("Incorrect locale information provided")); + parsed = parse_language_tag(&canonical) + .unwrap_or_else(|| throw_range_error("Incorrect locale information provided")); + canonicalize_aliases(&mut parsed); + } let proto = super::constructor_target_prototype(closure); make_locale_instance(proto.to_bits(), &parsed) diff --git a/crates/perry-runtime/src/intl/locale/likely_subtags.rs b/crates/perry-runtime/src/intl/locale/likely_subtags.rs index 16a4624734..75ceaaa36c 100644 --- a/crates/perry-runtime/src/intl/locale/likely_subtags.rs +++ b/crates/perry-runtime/src/intl/locale/likely_subtags.rs @@ -9,7 +9,24 @@ use super::ParsedLocale; +#[cfg(feature = "intl-locale")] +fn transform_with_icu(p: &mut ParsedLocale, maximize: bool) { + let Ok(mut locale) = super::full_string(p).parse::() else { + return; + }; + let expander = icu_locale::LocaleExpander::new_extended(); + if maximize { + let _ = expander.maximize(&mut locale.id); + } else { + let _ = expander.minimize(&mut locale.id); + } + if let Some(parsed) = super::parse_language_tag(&locale.to_string()) { + *p = parsed; + } +} + /// `language -> (script, region)` — the maximal expansion of a bare language. +#[cfg(not(feature = "intl-locale"))] const LANG: &[(&str, &str, &str)] = &[ ("en", "Latn", "US"), ("es", "Latn", "ES"), @@ -97,6 +114,7 @@ const LANG: &[(&str, &str, &str)] = &[ /// `(language, region) -> script` overrides where the region disambiguates the /// script (e.g. `zh-TW` is `Hant`, not the bare-`zh` default `Hans`). +#[cfg(not(feature = "intl-locale"))] const LANG_REGION: &[(&str, &str, &str)] = &[ ("zh", "TW", "Hant"), ("zh", "HK", "Hant"), @@ -109,6 +127,7 @@ const LANG_REGION: &[(&str, &str, &str)] = &[ /// `script -> language` — the most likely language for a script, used to fill a /// `und`-language tag during maximization. +#[cfg(not(feature = "intl-locale"))] const SCRIPT_LANG: &[(&str, &str)] = &[ ("Latn", "en"), ("Cyrl", "ru"), @@ -128,12 +147,14 @@ const SCRIPT_LANG: &[(&str, &str)] = &[ ("Beng", "bn"), ]; +#[cfg(not(feature = "intl-locale"))] fn lang_defaults(lang: &str) -> Option<(&'static str, &'static str)> { LANG.iter() .find(|(l, _, _)| *l == lang) .map(|(_, s, r)| (*s, *r)) } +#[cfg(not(feature = "intl-locale"))] fn script_for_region(lang: &str, region: &str) -> Option<&'static str> { LANG_REGION .iter() @@ -141,6 +162,7 @@ fn script_for_region(lang: &str, region: &str) -> Option<&'static str> { .map(|(_, _, s)| *s) } +#[cfg(not(feature = "intl-locale"))] fn lang_for_script(script: &str) -> Option<&'static str> { SCRIPT_LANG .iter() @@ -150,6 +172,7 @@ fn lang_for_script(script: &str) -> Option<&'static str> { /// Fully expand `(language, script, region)`, filling missing script/region from /// the table. Returns the (possibly unchanged) maximal triple. +#[cfg(not(feature = "intl-locale"))] fn maximize_triple( language: &str, script: Option, @@ -189,41 +212,58 @@ fn maximize_triple( /// `Intl.Locale.prototype.maximize`: add the most likely script and region. pub(super) fn maximize(p: &mut ParsedLocale) { - let (lang, script, region) = maximize_triple(&p.language, p.script.clone(), p.region.clone()); - p.language = lang; - p.script = script; - p.region = region; + #[cfg(feature = "intl-locale")] + { + transform_with_icu(p, true); + return; + } + #[cfg(not(feature = "intl-locale"))] + { + let (lang, script, region) = + maximize_triple(&p.language, p.script.clone(), p.region.clone()); + p.language = lang; + p.script = script; + p.region = region; + } } /// `Intl.Locale.prototype.minimize`: remove script/region that the /// likely-subtags expansion would re-add. Chooses the shortest base subtags /// whose maximization round-trips to the same maximal triple. pub(super) fn minimize(p: &mut ParsedLocale) { - let max = maximize_triple(&p.language, p.script.clone(), p.region.clone()); - // Minimization operates on the fully-resolved tag, so the result language is - // always the maximal language (e.g. `und-Latn` minimizes to `en`). - let lang = max.0.clone(); - p.language = lang.clone(); - - // 1. language alone. - if maximize_triple(&lang, None, None) == max { - p.script = None; - p.region = None; - return; - } - // 2. language + region. - if max.2.is_some() && maximize_triple(&lang, None, max.2.clone()) == max { - p.script = None; - p.region = max.2.clone(); + #[cfg(feature = "intl-locale")] + { + transform_with_icu(p, false); return; } - // 3. language + script. - if max.1.is_some() && maximize_triple(&lang, max.1.clone(), None) == max { - p.script = max.1.clone(); - p.region = None; - return; + #[cfg(not(feature = "intl-locale"))] + { + let max = maximize_triple(&p.language, p.script.clone(), p.region.clone()); + // Minimization operates on the fully-resolved tag, so the result language is + // always the maximal language (e.g. `und-Latn` minimizes to `en`). + let lang = max.0.clone(); + p.language = lang.clone(); + + // 1. language alone. + if maximize_triple(&lang, None, None) == max { + p.script = None; + p.region = None; + return; + } + // 2. language + region. + if max.2.is_some() && maximize_triple(&lang, None, max.2.clone()) == max { + p.script = None; + p.region = max.2.clone(); + return; + } + // 3. language + script. + if max.1.is_some() && maximize_triple(&lang, max.1.clone(), None) == max { + p.script = max.1.clone(); + p.region = None; + return; + } + // 4. keep the full maximal triple. + p.script = max.1; + p.region = max.2; } - // 4. keep the full maximal triple. - p.script = max.1; - p.region = max.2; } diff --git a/crates/perry-runtime/src/intl/locales.rs b/crates/perry-runtime/src/intl/locales.rs index e37eacee34..22dbe4cb58 100644 --- a/crates/perry-runtime/src/intl/locales.rs +++ b/crates/perry-runtime/src/intl/locales.rs @@ -3,45 +3,10 @@ //! under the per-file LOC ceiling. Canonicalization itself lives in //! [`super::canonicalize_language_tag`]. -use super::{ - array_ptr_from_value, canonicalize_language_tag, get_field, get_number_field, - 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 super::{locales_from_value, string_value, throw_range_error, value_to_string}; +use crate::array::{js_array_alloc, js_array_push_f64}; use crate::closure::ClosureHeader; -use crate::value::{js_nanbox_pointer, JSValue}; - -/// The ECMA-402 element-type guard inside CanonicalizeLocaleList: each element -/// must be a String or an Object, else `TypeError`. A Locale/other object is -/// coerced via `ToString` (an `Intl.Locale` stringifies to its canonical id). -fn locale_list_element_tag(value: f64) -> String { - let js = JSValue::from_bits(value.to_bits()); - 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); - } - throw_type_error("locale must be a String or Object"); -} - -fn push_canonical_locale(seen: &mut Vec, tag: &str) { - let Some(canonical) = canonicalize_language_tag(tag) else { - throw_invalid_language_tag(tag); - }; - if !seen.iter().any(|existing| existing == &canonical) { - seen.push(canonical); - } -} +use crate::value::js_nanbox_pointer; pub(super) fn canonical_locales_array(list: &[String]) -> f64 { let mut arr = js_array_alloc(list.len() as u32); @@ -54,54 +19,10 @@ pub(super) fn canonical_locales_array(list: &[String]) -> f64 { /// `Intl.getCanonicalLocales(locales)` — CanonicalizeLocaleList then /// CreateArrayFromList. `undefined` → `[]`; a String → a single-element list; /// `null` → `TypeError`; an Array (or array-like Object) → its elements -/// canonicalized and de-duplicated, in order; any other primitive → `[]` -/// (`ToObject` yields a wrapper with no integer-indexed entries). +/// canonicalized and de-duplicated, in order. Other primitives are ToObject- +/// wrapped, so inherited array-like properties remain observable. pub(super) fn get_canonical_locales(locales: f64) -> f64 { - let js = JSValue::from_bits(locales.to_bits()); - let mut seen: Vec = Vec::new(); - - if js.is_undefined() { - return canonical_locales_array(&seen); - } - if js.is_null() { - throw_type_error("Cannot convert undefined or null to object"); - } - if js.is_any_string() { - let tag = string_from_string_value(locales).unwrap_or_default(); - 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 { - let tag = locale_list_element_tag(js_array_get_f64(arr, i)); - push_canonical_locale(&mut seen, &tag); - } - return canonical_locales_array(&seen); - } - if let Some(obj) = object_ptr_from_value(locales) { - // Generic array-like: iterate `O[0..length]`. - let len = get_number_field(obj, "length") - .filter(|n| n.is_finite() && *n > 0.0) - .map(|n| n as u32) - .unwrap_or(0); - for i in 0..len { - let tag = locale_list_element_tag(get_field(obj, &i.to_string())); - push_canonical_locale(&mut seen, &tag); - } - return canonical_locales_array(&seen); - } - // Other primitives (number/boolean/symbol/bigint): ToObject succeeds but the - // wrapper has length 0 — an empty list, no throw. - canonical_locales_array(&seen) + canonical_locales_array(&locales_from_value(locales)) } pub(super) extern "C" fn get_canonical_locales_thunk( diff --git a/crates/perry-runtime/src/intl/method_install.rs b/crates/perry-runtime/src/intl/method_install.rs new file mode 100644 index 0000000000..7a288e17b5 --- /dev/null +++ b/crates/perry-runtime/src/intl/method_install.rs @@ -0,0 +1,131 @@ +use super::*; + +pub(super) fn install_bound_instance_function( + obj: *mut ObjectHeader, + name: &str, + func_ptr: *const u8, + arity: u32, +) -> *mut ClosureHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(obj); + let closure = crate::closure::js_closure_alloc(func_ptr, 1); + if closure.is_null() { + return closure; + } + let closure = scope.root_raw_mut_ptr(closure); + crate::closure::js_register_closure_arity(func_ptr, arity); + closure.with_mut_ptr(|closure| { + obj.with_mut_ptr(|obj: *mut ObjectHeader| { + crate::closure::js_closure_set_capture_f64(closure, 0, js_nanbox_pointer(obj as i64)) + }) + }); + closure.with_mut_ptr(|closure| crate::object::set_bound_native_closure_name(closure, name)); + closure.with_mut_ptr::(|closure| { + crate::object::set_builtin_closure_length(closure as usize, arity) + }); + // A bound Intl instance method (`nf.format`, `nf.resolvedOptions`, …) is a + // built-in non-constructor function: it has NO `[[Construct]]` and therefore + // no own `prototype` property (ECMA-262 §17 — built-in functions that aren't + // constructors don't get the auto-created `.prototype`). Flag it so + // `function_would_have_own_prototype` / the `new` path treat it like any + // other builtin (`Math.max`), matching `format-function-builtin.js`. + closure.with_mut_ptr::(|closure| { + crate::object::set_builtin_closure_non_constructable(closure as usize) + }); + closure.with_mut_ptr(|closure: *mut ClosureHeader| { + crate::object::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + PropertyAttrs::new(false, false, true), + ) + }); + closure.with_mut_ptr(|closure: *mut ClosureHeader| { + crate::object::set_builtin_property_attrs( + closure as usize, + "length".to_string(), + PropertyAttrs::new(false, false, true), + ) + }); + let closure_value = + closure.with_mut_ptr(|closure: *mut ClosureHeader| js_nanbox_pointer(closure as i64)); + obj.with_mut_ptr(|obj| set_field(obj, name, closure_value)); + obj.with_mut_ptr(|obj| set_builtin_attrs(obj, name, PropertyAttrs::new(true, false, true))); + closure.with_mut_ptr(|closure| closure) +} + +pub(super) fn install_bound_instance_function_from_handle( + obj: &crate::gc::RuntimeHandle<'_>, + name: &str, + func_ptr: *const u8, + arity: u32, +) -> *mut ClosureHeader { + obj.with_mut_ptr(|obj| install_bound_instance_function(obj, name, func_ptr, arity)) +} + +pub(super) fn install_function( + owner: *mut ObjectHeader, + name: &str, + func_ptr: *const u8, + call_arity: u32, + length: u32, + has_rest: bool, +) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let owner = scope.root_raw_mut_ptr(owner); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return undefined(); + } + let closure = scope.root_raw_mut_ptr(closure); + if has_rest { + crate::closure::js_register_closure_rest(func_ptr, call_arity); + } else { + crate::closure::js_register_closure_arity(func_ptr, call_arity); + } + closure.with_mut_ptr(|closure| crate::object::set_bound_native_closure_name(closure, name)); + closure.with_mut_ptr::(|closure| { + crate::object::set_builtin_closure_length(closure as usize, length) + }); + // Intl prototype methods (`formatToParts`, `resolvedOptions`, …), the static + // `supportedLocalesOf`, and the this-based instance methods + // (`formatRange`/`formatRangeToParts`) installed through here are all + // built-in non-constructor functions: no `[[Construct]]`, hence no own + // `prototype` property (`builtin.js` asserts `hasOwnProperty("prototype")` + // is false and `isConstructor` is false). Flag them like any other builtin. + closure.with_mut_ptr::(|closure| { + crate::object::set_builtin_closure_non_constructable(closure as usize) + }); + closure.with_mut_ptr(|closure: *mut ClosureHeader| { + crate::object::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + PropertyAttrs::new(false, false, true), + ) + }); + closure.with_mut_ptr(|closure: *mut ClosureHeader| { + crate::object::set_builtin_property_attrs( + closure as usize, + "length".to_string(), + PropertyAttrs::new(false, false, true), + ) + }); + let value = + closure.with_mut_ptr(|closure: *mut ClosureHeader| js_nanbox_pointer(closure as i64)); + owner.with_mut_ptr(|owner| set_field(owner, name, value)); + owner.with_mut_ptr(|owner| { + set_builtin_attrs(owner, name, PropertyAttrs::new(true, false, true)) + }); + value +} + +pub(super) fn install_function_from_handle( + owner: &crate::gc::RuntimeHandle<'_>, + name: &str, + func_ptr: *const u8, + call_arity: u32, + length: u32, + has_rest: bool, +) -> f64 { + owner + .with_mut_ptr(|owner| install_function(owner, name, func_ptr, call_arity, length, has_rest)) +} diff --git a/crates/perry-runtime/src/intl/number_format_options.rs b/crates/perry-runtime/src/intl/number_format_options.rs index 26e0fc8566..ed4cdda077 100644 --- a/crates/perry-runtime/src/intl/number_format_options.rs +++ b/crates/perry-runtime/src/intl/number_format_options.rs @@ -6,9 +6,13 @@ use crate::value::JSValue; /// Read, validate, and store the NumberFormat option slots (ECMA-402 /// CreateNumberFormat / SetNumberFormatUnitOptions / SetNumberFormatDigitOptions). pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, options: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let options_handle = scope.root_nanbox_f64(options); + let current_options = || options_handle.get_nanbox_f64(); // CoerceOptionsToObject: `null` throws; `undefined` behaves as an empty // null-prototype object (our readers already treat non-objects as empty). - if JSValue::from_bits(options.to_bits()).is_null() { + if JSValue::from_bits(current_options().to_bits()).is_null() { throw_type_error("Cannot convert undefined or null to object"); } @@ -18,7 +22,7 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti // constructor-option-read-order.js asserts (localeMatcher before // numberingSystem) and propagates a throwing localeMatcher getter. let _ = get_string_option_enum( - options, + current_options(), "localeMatcher", &["lookup", "best fit"], "best fit", @@ -28,7 +32,7 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti // then run ResolveLocale for the `nu` key — reconciling the option with the // requested locale's `-u-nu-` keyword and updating the resolved locale so // `resolvedOptions().locale` reflects only the supported value actually used. - let opt_ns = match get_option_string(options, "numberingSystem") { + let opt_ns = match get_option_string(current_options(), "numberingSystem") { Some(value) => { let lower = value.to_ascii_lowercase(); if !is_well_formed_numbering_system(&lower) { @@ -41,24 +45,28 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti None => None, }; let (resolved_locale, numbering) = resolve_numbering_system(locale, opt_ns.as_deref()); - set_internal_field(obj, KEY_LOCALE, string_value(&resolved_locale)); - set_internal_field(obj, KEY_NF_NUMBERING, string_value(&numbering)); + set_internal_field_from_raw_handle(&obj_handle, KEY_LOCALE, string_value(&resolved_locale)); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_NUMBERING, string_value(&numbering)); // SetNumberFormatUnitOptions. let style = get_string_option_enum( - options, + current_options(), "style", &["decimal", "percent", "currency", "unit"], "decimal", ); - set_internal_field(obj, KEY_STYLE, string_value(&style)); + set_internal_field_from_raw_handle(&obj_handle, KEY_STYLE, string_value(&style)); - let currency = get_option_string(options, "currency"); + let currency = get_option_string(current_options(), "currency"); if let Some(code) = ¤cy { if !is_well_formed_currency_code(code) { throw_range_error(&format!("Invalid currency code : {code}")); } - set_internal_field(obj, KEY_CURRENCY, string_value(&code.to_ascii_uppercase())); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_CURRENCY, + string_value(&code.to_ascii_uppercase()), + ); } // Throw TypeError for missing currency BEFORE reading currencyDisplay / // currencySign — so proxy-get traps on those keys are never triggered when @@ -67,40 +75,48 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti throw_type_error("Currency code is required with currency style."); } let currency_display = get_string_option_enum( - options, + current_options(), "currencyDisplay", &["code", "symbol", "narrowSymbol", "name"], "symbol", ); let currency_sign = get_string_option_enum( - options, + current_options(), "currencySign", &["standard", "accounting"], "standard", ); - set_internal_field( - obj, + set_internal_field_from_raw_handle( + &obj_handle, KEY_NF_CURRENCY_DISPLAY, string_value(¤cy_display), ); - set_internal_field(obj, KEY_NF_CURRENCY_SIGN, string_value(¤cy_sign)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NF_CURRENCY_SIGN, + string_value(¤cy_sign), + ); - let unit = get_option_string(options, "unit"); + let unit = get_option_string(current_options(), "unit"); if let Some(u) = &unit { if !is_well_formed_unit_identifier(u) { throw_range_error(&format!( "Value {u} out of range for Intl.NumberFormat options property unit" )); } - set_internal_field(obj, KEY_NF_UNIT, string_value(u)); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_UNIT, string_value(u)); } let unit_display = get_string_option_enum( - options, + current_options(), "unitDisplay", &["short", "narrow", "long"], "short", ); - set_internal_field(obj, KEY_NF_UNIT_DISPLAY, string_value(&unit_display)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NF_UNIT_DISPLAY, + string_value(&unit_display), + ); if style == "unit" && unit.is_none() { throw_type_error("unit is required with unit style."); @@ -108,33 +124,37 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti // notation (read before the digit options per the spec order). let notation = get_string_option_enum( - options, + current_options(), "notation", &["standard", "scientific", "engineering", "compact"], "standard", ); - set_internal_field(obj, KEY_NF_NOTATION, string_value(¬ation)); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_NOTATION, string_value(¬ation)); // SetNumberFormatDigitOptions — the GetOption reads run in the exact // ECMA-402 order asserted by constructor-option-read-order.js: // minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits, // minimumSignificantDigits, maximumSignificantDigits, roundingIncrement, // roundingMode, roundingPriority, trailingZeroDisplay. - let min_int = - get_int_option_in_range(options, "minimumIntegerDigits", 1.0, 21.0).unwrap_or(1.0); - let min_frac_opt = get_int_option_in_range(options, "minimumFractionDigits", 0.0, 100.0); - let max_frac_opt = get_int_option_in_range(options, "maximumFractionDigits", 0.0, 100.0); - let min_sig_opt = get_int_option_in_range(options, "minimumSignificantDigits", 1.0, 21.0); - let max_sig_opt = get_int_option_in_range(options, "maximumSignificantDigits", 1.0, 21.0); + let min_int = get_int_option_in_range(current_options(), "minimumIntegerDigits", 1.0, 21.0) + .unwrap_or(1.0); + let min_frac_opt = + get_int_option_in_range(current_options(), "minimumFractionDigits", 0.0, 100.0); + let max_frac_opt = + get_int_option_in_range(current_options(), "maximumFractionDigits", 0.0, 100.0); + let min_sig_opt = + get_int_option_in_range(current_options(), "minimumSignificantDigits", 1.0, 21.0); + let max_sig_opt = + get_int_option_in_range(current_options(), "maximumSignificantDigits", 1.0, 21.0); // roundingIncrement is read before roundingMode/roundingPriority and is // ToNumber-coerced (so `{ valueOf }` objects work) then checked against the // sanctioned increment set — a [1, 5000] range alone would wrongly admit // values like 3 or 5000.1. - let rounding_increment = read_rounding_increment(options); + let rounding_increment = read_rounding_increment(current_options()); let rounding_mode = enum_option_strict( - options, + current_options(), "roundingMode", &[ "ceil", @@ -150,19 +170,19 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti "halfExpand", ); let mut rounding_priority = get_string_option_enum( - options, + current_options(), "roundingPriority", &["auto", "morePrecision", "lessPrecision"], "auto", ); let trailing_zero = get_string_option_enum( - options, + current_options(), "trailingZeroDisplay", &["auto", "stripIfInteger"], "auto", ); - set_internal_field(obj, KEY_NF_MIN_INT, min_int); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_MIN_INT, min_int); // The currency-specific digit defaults only apply to "standard" notation // (ECMA-402 SetNumberFormatDigitOptions step 19-20) — compact/engineering/ @@ -256,10 +276,10 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti } } - set_internal_field(obj, KEY_NF_MIN_SIG, min_sig as f64); - set_internal_field(obj, KEY_NF_MAX_SIG, max_sig as f64); - set_internal_field(obj, KEY_NF_MIN_FRAC, min_frac as f64); - set_internal_field(obj, KEY_MAX_FRACTION_DIGITS, max_frac as f64); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_MIN_SIG, min_sig as f64); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_MAX_SIG, max_sig as f64); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_MIN_FRAC, min_frac as f64); + set_internal_field_from_raw_handle(&obj_handle, KEY_MAX_FRACTION_DIGITS, max_frac as f64); // Digit display mode: "fraction" | "significant" | "both" (compact default). let digit_mode = if has_sd && !has_fd { @@ -280,42 +300,66 @@ pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, opti }; // Compact's significant defaults are 1–2 when not explicitly given. if digit_mode == "both" { - set_internal_field(obj, KEY_NF_MIN_SIG, 1.0); - set_internal_field(obj, KEY_NF_MAX_SIG, 2.0); - set_internal_field(obj, KEY_NF_MIN_FRAC, 0.0); - set_internal_field(obj, KEY_MAX_FRACTION_DIGITS, 0.0); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_MIN_SIG, 1.0); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_MAX_SIG, 2.0); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_MIN_FRAC, 0.0); + set_internal_field_from_raw_handle(&obj_handle, KEY_MAX_FRACTION_DIGITS, 0.0); } - set_internal_field(obj, KEY_NF_USE_SIG, string_value(digit_mode)); + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_USE_SIG, string_value(digit_mode)); - set_internal_field(obj, KEY_NF_ROUNDING_INCREMENT, rounding_increment); - set_internal_field(obj, KEY_NF_ROUNDING_MODE, string_value(&rounding_mode)); - set_internal_field( - obj, + set_internal_field_from_raw_handle(&obj_handle, KEY_NF_ROUNDING_INCREMENT, rounding_increment); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NF_ROUNDING_MODE, + string_value(&rounding_mode), + ); + set_internal_field_from_raw_handle( + &obj_handle, KEY_NF_ROUNDING_PRIORITY, string_value(&rounding_priority), ); - set_internal_field(obj, KEY_NF_TRAILING_ZERO, string_value(&trailing_zero)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NF_TRAILING_ZERO, + string_value(&trailing_zero), + ); // compactDisplay, useGrouping, signDisplay. - let compact_display = - get_string_option_enum(options, "compactDisplay", &["short", "long"], "short"); - set_internal_field(obj, KEY_NF_COMPACT_DISPLAY, string_value(&compact_display)); + let compact_display = get_string_option_enum( + current_options(), + "compactDisplay", + &["short", "long"], + "short", + ); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NF_COMPACT_DISPLAY, + string_value(&compact_display), + ); let default_grouping = if notation == "compact" { "min2" } else { "auto" }; - let use_grouping = get_use_grouping_option(options, default_grouping); - set_internal_field(obj, KEY_NF_USE_GROUPING, string_value(&use_grouping)); + let use_grouping = get_use_grouping_option(current_options(), default_grouping); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NF_USE_GROUPING, + string_value(&use_grouping), + ); let sign_display = get_string_option_enum( - options, + current_options(), "signDisplay", &["auto", "never", "always", "exceptZero", "negative"], "auto", ); - set_internal_field(obj, KEY_NF_SIGN_DISPLAY, string_value(&sign_display)); + set_internal_field_from_raw_handle( + &obj_handle, + KEY_NF_SIGN_DISPLAY, + string_value(&sign_display), + ); } /// GetNumberOption(options, "roundingIncrement", 1, 5000, 1) followed by the diff --git a/crates/perry-runtime/src/intl/rooted_fields.rs b/crates/perry-runtime/src/intl/rooted_fields.rs new file mode 100644 index 0000000000..3c261971b7 --- /dev/null +++ b/crates/perry-runtime/src/intl/rooted_fields.rs @@ -0,0 +1,94 @@ +use super::*; + +pub(super) fn get_field(value: *const ObjectHeader, key: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_raw_const_ptr(value); + let key = scope.root_string_ptr(js_string_from_bytes(key.as_ptr(), key.len() as u32)); + value.with_const_ptr(|value| { + key.with_const_ptr(|key| js_object_get_field_by_name_f64(value, key)) + }) +} + +pub(super) fn get_field_from_raw_handle(value: &crate::gc::RuntimeHandle<'_>, key: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let key = scope.root_string_ptr(js_string_from_bytes(key.as_ptr(), key.len() as u32)); + value.with_const_ptr::(|value| { + key.with_const_ptr(|key| js_object_get_field_by_name_f64(value, key)) + }) +} + +pub(super) fn get_string_field_from_raw_handle( + value: &crate::gc::RuntimeHandle<'_>, + key: &str, +) -> Option { + string_from_string_value(get_field_from_raw_handle(value, key)) +} + +pub(super) fn set_internal_field_from_raw_handle( + value: &crate::gc::RuntimeHandle<'_>, + key: &str, + field: f64, +) { + value.with_mut_ptr(|value| set_internal_field(value, key, field)); +} + +pub(super) fn get_field_from_value_handle(value: &crate::gc::RuntimeHandle<'_>, key: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let key = scope.root_string_ptr(js_string_from_bytes(key.as_ptr(), key.len() as u32)); + let current = value.get_nanbox_f64(); + let Some(object) = object_ptr_from_value(current) else { + return undefined(); + }; + key.with_const_ptr(|key| js_object_get_field_by_name_f64(object, key)) +} + +pub(super) fn set_field(obj: *mut ObjectHeader, key: &str, value: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(obj); + let value = scope.root_nanbox_f64(value); + let key = scope.root_string_ptr(js_string_from_bytes(key.as_ptr(), key.len() as u32)); + obj.with_mut_ptr(|obj| { + key.with_const_ptr(|key| js_object_set_field_by_name(obj, key, value.get_nanbox_f64())) + }); +} + +pub(super) fn set_builtin_attrs(obj: *mut ObjectHeader, key: &str, attrs: PropertyAttrs) { + set_builtin_property_attrs(obj as usize, key.to_string(), attrs); +} + +pub(super) fn set_internal_field(obj: *mut ObjectHeader, key: &str, value: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(obj); + obj.with_mut_ptr(|obj| set_field(obj, key, value)); + obj.with_mut_ptr(|obj| set_builtin_attrs(obj, key, PropertyAttrs::new(true, false, true))); +} + +pub(super) fn get_string_field(obj: *const ObjectHeader, key: &str) -> Option { + string_from_string_value(get_field(obj, key)) +} + +pub(super) fn get_number_field(obj: *const ObjectHeader, key: &str) -> Option { + let value = get_field(obj, key); + let js = JSValue::from_bits(value.to_bits()); + if js.is_undefined() || js.is_null() { + None + } else { + Some(js.to_number()) + } +} + +pub(super) fn get_option_value(options: f64, key: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let options = scope.root_nanbox_f64(options); + let key = scope.root_string_ptr(js_string_from_bytes(key.as_ptr(), key.len() as u32)); + if crate::proxy::js_proxy_is_proxy(options.get_nanbox_f64()) != 0 { + return crate::proxy::js_proxy_get( + options.get_nanbox_f64(), + key.with_mut_ptr(|key| f64::from_bits(JSValue::string_ptr(key).bits())), + ); + } + let Some(obj) = object_ptr_from_value(options.get_nanbox_f64()) else { + return undefined(); + }; + key.with_const_ptr(|key| js_object_get_field_by_name_f64(obj, key)) +} diff --git a/crates/perry-runtime/src/intl/segmenter.rs b/crates/perry-runtime/src/intl/segmenter.rs index 52d59bac24..6ba71870ec 100644 --- a/crates/perry-runtime/src/intl/segmenter.rs +++ b/crates/perry-runtime/src/intl/segmenter.rs @@ -178,18 +178,76 @@ pub(crate) fn build_segments(granularity: &str, value: f64) -> f64 { index = end; } } - let segments = arr as *mut ObjectHeader; - set_internal_field(segments, KEY_SEGMENTS_BRAND, string_value(SEGMENTS_BRAND)); - set_internal_field(segments, KEY_SEGMENTS_LENGTH, index as f64); - install_function( - segments, - "containing", - segmenter_containing_thunk as *const u8, - 1, - 1, - false, - ); - js_nanbox_pointer(arr as i64) + let scope = crate::gc::RuntimeHandleScope::new(); + let segments = scope.root_raw_mut_ptr(arr as *mut ObjectHeader); + let brand = string_value(SEGMENTS_BRAND); + segments.with_mut_ptr(|segments| set_internal_field(segments, KEY_SEGMENTS_BRAND, brand)); + segments + .with_mut_ptr(|segments| set_internal_field(segments, KEY_SEGMENTS_LENGTH, index as f64)); + segments.with_mut_ptr(|segments| { + install_function( + segments, + "containing", + segmenter_containing_thunk as *const u8, + 1, + 1, + false, + ) + }); + install_segments_iterator(&segments); + segments.with_mut_ptr(|segments: *mut ObjectHeader| js_nanbox_pointer(segments as i64)) +} + +fn install_segments_iterator(segments: &crate::gc::RuntimeHandle<'_>) { + let symbol = crate::symbol::well_known_symbol("iterator"); + if symbol.is_null() { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let symbol = scope.root_raw_mut_ptr(symbol); + let closure = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc( + segments_iterator_thunk as *const u8, + 0, + )); + if closure.with_mut_ptr(|closure: *mut ClosureHeader| closure.is_null()) { + return; + } + crate::closure::js_register_closure_arity(segments_iterator_thunk as *const u8, 0); + closure.with_mut_ptr::(|ptr| { + crate::object::set_bound_native_closure_name(ptr, "[Symbol.iterator]") + }); + closure.with_mut_ptr::(|ptr| { + crate::object::set_builtin_closure_length(ptr as usize, 0) + }); + let value = closure.with_mut_ptr::(|ptr| js_nanbox_pointer(ptr as i64)); + unsafe { + segments.with_mut_ptr(|segments: *mut ObjectHeader| { + symbol.with_const_ptr(|symbol: *const u8| { + crate::symbol::js_object_set_symbol_property( + js_nanbox_pointer(segments as i64), + f64::from_bits(JSValue::pointer(symbol).bits()), + value, + ) + }) + }); + } + segments.with_mut_ptr(|segments: *mut ObjectHeader| { + symbol.with_const_ptr(|symbol: *const u8| { + crate::symbol::set_symbol_property_attrs( + segments as usize, + symbol as usize, + PropertyAttrs::new(true, false, true), + ) + }) + }); +} + +extern "C" fn segments_iterator_thunk(_closure: *const ClosureHeader) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let segments = scope.root_raw_const_ptr(segments_from_this()); + segments.with_const_ptr(|segments: *const crate::ArrayHeader| { + crate::array::array_values_iter(js_nanbox_pointer(segments as i64)) + }) } fn segments_from_this() -> *const crate::ArrayHeader { diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index bebfa9038b..1e0409c74d 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -624,6 +624,11 @@ fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id } } +fn class_parent_prototype_bits(value: f64) -> Option { + let bits = value.to_bits(); + (bits == crate::value::TAG_NULL || (bits >> 48) == 0x7FFD).then_some(bits) +} + pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { // #7757: a specialization answers with its generic's prototype. let class_id = decl_prototype_identity_id(class_id); @@ -711,8 +716,9 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; } - let dynamic_parent = js_get_dynamic_parent_value(class_id); - let null_heritage = dynamic_parent.to_bits() == crate::value::TAG_NULL; + let scope = crate::gc::RuntimeHandleScope::new(); + let dynamic_parent = scope.root_nanbox_f64(js_get_dynamic_parent_value(class_id)); + let null_heritage = dynamic_parent.get_nanbox_f64().to_bits() == crate::value::TAG_NULL; let parent_proto_bits = if null_heritage { // A class extending null creates a prototype object whose // [[Prototype]] is null, not Object.prototype. Record TAG_NULL @@ -720,19 +726,50 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { // Object.prototype default. Some(crate::value::TAG_NULL) } else { - get_parent_class_id(class_id) + let registered_parent_proto = get_parent_class_id(class_id) .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) .and_then(|parent_id| { let parent_proto = class_decl_prototype_value(parent_id); let parent_bits = parent_proto.to_bits(); ((parent_bits >> 48) == 0x7FFD).then_some(parent_bits) - }) - .or_else(global_object_prototype_bits) + }); + if registered_parent_proto.is_some() { + registered_parent_proto + } else { + // A runtime function-valued superclass (including Intl service + // constructors) has no class-id edge. Link the declared prototype + // to the parent's own `.prototype` exactly once, while this fresh + // class prototype is initialized. Construction must never rewrite + // this edge after user code mutates it. + let parent = JSValue::from_bits(dynamic_parent.get_nanbox_f64().to_bits()); + if parent.is_pointer() { + let parent_addr = parent.as_pointer::() as usize; + if crate::closure::is_closure_ptr(parent_addr) { + let parent_proto = + crate::closure::closure_get_dynamic_prop(parent_addr, "prototype"); + if let Some(bits) = class_parent_prototype_bits(parent_proto) { + Some(bits) + } else { + super::super::object_ops::throw_object_type_error( + b"Class extends value does not have valid prototype property", + ); + } + } else { + global_object_prototype_bits() + } + } else { + global_object_prototype_bits() + } + } }; if let Some(bits) = parent_proto_bits { - super::super::prototype_chain::object_set_static_prototype(proto as usize, bits); + let proto = class_decl_prototype_object(class_id); + if !proto.is_null() { + super::super::prototype_chain::object_set_static_prototype(proto as usize, bits); + } } + let proto = class_decl_prototype_object(class_id); crate::value::js_nanbox_pointer(proto as i64) } @@ -883,3 +920,23 @@ mod class_dynamic_prop_store_tests { assert_eq!(stored(cid, "k"), Some(3.0)); } } + +#[cfg(test)] +mod class_parent_prototype_tests { + use super::*; + + #[test] + fn only_object_and_null_parent_prototypes_are_valid() { + let object = f64::from_bits(crate::value::POINTER_TAG | 0x1234); + assert_eq!(class_parent_prototype_bits(object), Some(object.to_bits())); + assert_eq!( + class_parent_prototype_bits(f64::from_bits(crate::value::TAG_NULL)), + Some(crate::value::TAG_NULL) + ); + assert_eq!( + class_parent_prototype_bits(f64::from_bits(crate::value::TAG_UNDEFINED)), + None + ); + assert_eq!(class_parent_prototype_bits(1.0), None); + } +} diff --git a/crates/perry-stdlib/src/net/mod.rs b/crates/perry-stdlib/src/net/mod.rs index b24c6b793d..b2d832e096 100644 --- a/crates/perry-stdlib/src/net/mod.rs +++ b/crates/perry-stdlib/src/net/mod.rs @@ -543,6 +543,7 @@ unsafe fn get_object_bool_field(obj_f64: f64, field_name: &str) -> Option /// mirrors `crates/perry-stdlib/src/sqlite.rs::build_packed_keys`. unsafe fn build_error_object(msg: &str) -> f64 { use perry_runtime::JSValue; + let scope = perry_runtime::gc::RuntimeHandleScope::new(); let keys = ["message", "code", "name"]; let mut packed = Vec::new(); for key in keys { @@ -554,17 +555,27 @@ unsafe fn build_error_object(msg: &str) -> f64 { shape_id = shape_id.wrapping_mul(31).wrapping_add(b as u32); } shape_id = shape_id.wrapping_add(3); - let s_msg = perry_runtime::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let obj = perry_runtime::js_object_alloc_with_shape( + let s_msg = scope.root_string_ptr(perry_runtime::js_string_from_bytes( + msg.as_ptr(), + msg.len() as u32, + )); + let obj_ptr = perry_runtime::js_object_alloc_with_shape( shape_id, 3, packed.as_ptr(), packed.len() as u32, ); - if obj.is_null() { - return f64::from_bits(0x7FFF_0000_0000_0000u64 | (s_msg as u64 & 0x0000_FFFF_FFFF_FFFF)); + if obj_ptr.is_null() { + return s_msg.with_const_ptr(|s_msg: *const perry_runtime::StringHeader| { + f64::from_bits(0x7FFF_0000_0000_0000u64 | (s_msg as u64 & 0x0000_FFFF_FFFF_FFFF)) + }); } - perry_runtime::js_object_set_field(obj, 0, JSValue::string_ptr(s_msg)); + let obj = scope.root_raw_mut_ptr(obj_ptr); + obj.with_mut_ptr(|obj| { + s_msg.with_mut_ptr(|s_msg| { + perry_runtime::js_object_set_field(obj, 0, JSValue::string_ptr(s_msg)) + }) + }); let code = if msg.starts_with("ERR_") { Some(msg) } else if msg.contains("UnknownIssuer") @@ -578,13 +589,25 @@ unsafe fn build_error_object(msg: &str) -> f64 { None }; if let Some(code) = code { - let code = perry_runtime::js_string_from_bytes(code.as_ptr(), code.len() as u32); - perry_runtime::js_object_set_field(obj, 1, JSValue::string_ptr(code)); + let code = scope.root_string_ptr(perry_runtime::js_string_from_bytes( + code.as_ptr(), + code.len() as u32, + )); + obj.with_mut_ptr(|obj| { + code.with_mut_ptr(|code| { + perry_runtime::js_object_set_field(obj, 1, JSValue::string_ptr(code)) + }) + }); } - let name = perry_runtime::js_string_from_bytes(b"Error".as_ptr(), 5); - perry_runtime::js_object_set_field(obj, 2, JSValue::string_ptr(name)); - let obj_bits = (obj as u64 & 0x0000_FFFF_FFFF_FFFF) | 0x7FFD_0000_0000_0000; - f64::from_bits(obj_bits) + let name = scope.root_string_ptr(perry_runtime::js_string_from_bytes(b"Error".as_ptr(), 5)); + obj.with_mut_ptr(|obj| { + name.with_mut_ptr(|name| { + perry_runtime::js_object_set_field(obj, 2, JSValue::string_ptr(name)) + }) + }); + obj.with_mut_ptr(|obj: *mut perry_runtime::ObjectHeader| { + f64::from_bits((obj as u64 & 0x0000_FFFF_FFFF_FFFF) | 0x7FFD_0000_0000_0000) + }) } fn next_id() -> i64 { @@ -1833,10 +1856,11 @@ pub unsafe extern "C" fn js_net_process_pending() -> i32 { // so user code can read `err.message`. Pre-fix the listener // received a raw NaN-boxed string and `err.message` came // back as `undefined`. - let err_f64 = build_error_object(&msg); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let error = scope.root_nanbox_f64(build_error_object(&msg)); for cb in cbs { if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, err_f64); + js_closure_call1(cb as *const ClosureHeader, error.get_nanbox_f64()); } } } diff --git a/crates/perry-transform/src/async_to_generator_tests.rs b/crates/perry-transform/src/async_to_generator_tests.rs index 7bb4b8759b..c5cec94737 100644 --- a/crates/perry-transform/src/async_to_generator_tests.rs +++ b/crates/perry-transform/src/async_to_generator_tests.rs @@ -6,8 +6,6 @@ use super::*; -use super::*; - // The minimal shape the collect scan matches: `async () => { await 1 }` // — `Expr::Closure { is_async, !is_generator }` whose body has an Await. fn async_closure_with_await(func_id: perry_hir::types::FuncId) -> Expr { diff --git a/crates/perry/src/commands/compile/collect_modules/feature_detect.rs b/crates/perry/src/commands/compile/collect_modules/feature_detect.rs index 6be7bed29f..e529a29c0a 100644 --- a/crates/perry/src/commands/compile/collect_modules/feature_detect.rs +++ b/crates/perry/src/commands/compile/collect_modules/feature_detect.rs @@ -448,11 +448,13 @@ pub(super) fn detect_optional_feature_usage( { ctx.uses_proc_ipc = true; } - // `Intl.getCanonicalLocales(...)` / `Intl.*.supportedLocalesOf(...)` gate - // `perry-runtime/intl-locale` (`icu_locale_core` BCP-47 canonicalization). - // Both lower with the method name as a `property` token. + // `Intl.Locale`, `Intl.getCanonicalLocales(...)`, and + // `Intl.*.supportedLocalesOf(...)` gate `perry-runtime/intl-locale` + // (ICU4X BCP-47 canonicalization + likely-subtag expansion). These lower + // with the constructor/method name as a `property` token. if hir_debug.contains("property: \"getCanonicalLocales\"") || hir_debug.contains("property: \"supportedLocalesOf\"") + || hir_debug.contains("property: \"Locale\"") { ctx.uses_intl_locale = true; } diff --git a/scripts/ci_e2e_scope.py b/scripts/ci_e2e_scope.py index 94e88edd69..a364c98be3 100755 --- a/scripts/ci_e2e_scope.py +++ b/scripts/ci_e2e_scope.py @@ -121,6 +121,7 @@ "constructor_recursion", "destructure_call_location", "i64_spec_ternary_recursion", + "ios_platform_api_lowering", "large_object_barriers", "loop_safepoint_purity", "macos_bundle_chdir_gate", @@ -143,6 +144,7 @@ "spec_abi_typed_array_local_length", "static_symbol_hygiene", "temp_root_operand_temporaries", + "typed_array_rmw_8692", "typed_shape_declared_at_allocation", "typed_shape_descriptor", "typed_shape_descriptors", diff --git a/scripts/unrooted_local_shape_baseline.json b/scripts/unrooted_local_shape_baseline.json index f8af29e696..986057a159 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -5,13 +5,13 @@ "crates/perry-ext-commander/src/lib.rs": 4, "crates/perry-ext-cron/src/lib.rs": 2, "crates/perry-ext-decimal/src/lib.rs": 1, - "crates/perry-ext-events/src/lib.rs": 23, + "crates/perry-ext-events/src/lib.rs": 21, "crates/perry-ext-events/src/module_iterators.rs": 2, "crates/perry-ext-events/src/tests.rs": 2, "crates/perry-ext-fastify/src/upgrade.rs": 4, "crates/perry-ext-fetch/src/lib.rs": 14, "crates/perry-ext-fetch/src/tests.rs": 14, - "crates/perry-ext-http/src/agent.rs": 4, + "crates/perry-ext-http/src/agent.rs": 3, "crates/perry-ext-http/src/client_request_surface.rs": 2, "crates/perry-ext-http/src/response_headers.rs": 1, "crates/perry-ext-http/src/server/handle_dispatch.rs": 2, @@ -35,15 +35,14 @@ "crates/perry-stdlib/src/cron.rs": 2, "crates/perry-stdlib/src/crypto/kdf.rs": 9, "crates/perry-stdlib/src/crypto/sign.rs": 22, - "crates/perry-stdlib/src/crypto/util.rs": 3, - "crates/perry-stdlib/src/crypto/x509.rs": 19, + "crates/perry-stdlib/src/crypto/util.rs": 2, "crates/perry-stdlib/src/domain.rs": 3, "crates/perry-stdlib/src/ethers.rs": 5, "crates/perry-stdlib/src/events.rs": 6, "crates/perry-stdlib/src/events/constructors.rs": 1, "crates/perry-stdlib/src/events/events_on.rs": 17, "crates/perry-stdlib/src/events/module_helpers.rs": 1, - "crates/perry-stdlib/src/events/once_helpers.rs": 4, + "crates/perry-stdlib/src/events/once_helpers.rs": 1, "crates/perry-stdlib/src/events/warnings.rs": 1, "crates/perry-stdlib/src/fetch/mod.rs": 6, "crates/perry-stdlib/src/ioredis.rs": 14, @@ -52,7 +51,6 @@ "crates/perry-stdlib/src/mysql2/pool.rs": 2, "crates/perry-stdlib/src/mysql2/result.rs": 43, "crates/perry-stdlib/src/mysql2/types.rs": 16, - "crates/perry-stdlib/src/net/mod.rs": 3, "crates/perry-stdlib/src/nodemailer.rs": 3, "crates/perry-stdlib/src/pg/result.rs": 14, "crates/perry-stdlib/src/pg/types.rs": 14, @@ -86,5 +84,5 @@ "crates/perry-stdlib/src/zlib.rs": 2 }, "schema_version": 2, - "total": 605 + "total": 576 }