From 978a8cf4e4477d2f3925dcc1ee0f6c4c8d11e3a6 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Sat, 4 Jul 2026 19:47:40 +0000 Subject: [PATCH] fix(intl): ToObject primitive options in SupportedLocalesOf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ECMA-402 SupportedLocales step 1.a is `? ToObject(options)`. When options is a non-object primitive (Boolean/Number/String/Symbol/BigInt), ToObject boxes it into a wrapper whose prototype chain reaches Object.prototype, so reading `localeMatcher` fires an Object.prototype getter for that key exactly once. Perry read option keys directly off the raw value, so a primitive options argument yielded every-key-absent and never consulted the prototype chain — the getter was never called (`called === 0`). Box a non-object primitive into a fresh empty object before the localeMatcher read (null is still rejected first). An empty object carries no own option keys, so the read resolves through the prototype chain, matching the boxed wrapper the spec observes. Fixes test262 supportedLocalesOf/options-toobject.js for Intl.Segmenter, Intl.ListFormat, and Intl.RelativeTimeFormat (3 cases). --- crates/perry-runtime/src/intl.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index 651a07e4c4..8c6d465ebc 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -878,6 +878,22 @@ 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. +fn to_object_for_options(options: f64) -> f64 { + if 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) +} + /// GetBooleanOption(options, key): `undefined` → `None`, otherwise ToBoolean. fn get_bool_option(options: f64, key: &str) -> Option { let value = get_option_value(options, key); @@ -1692,7 +1708,10 @@ fn supported_locales_array(locales: f64, options: f64) -> f64 { // lookup result. let requested = locales_from_value(locales); if !JSValue::from_bits(options.to_bits()).is_undefined() { - let options = coerce_options_reject_null(options); + // SupportedLocales step 1.a: ? ToObject(options). null throws; a + // primitive is boxed so the localeMatcher read fires an Object.prototype + // getter exactly once (options-toobject.js). + let options = to_object_for_options(coerce_options_reject_null(options)); let _ = enum_option_strict( options, "localeMatcher",