@@ -506,6 +506,14 @@ fn canonicalize_language_tag(tag: &str) -> Option<String> {
506506 }
507507}
508508
509+ /// `HasProperty(O, ToString(index))` — true when the integer-indexed property is
510+ /// present (own or inherited). Used to skip holes/absent indices in
511+ /// CanonicalizeLocaleList's array/array-like walk.
512+ fn js_has_index ( obj : f64 , index : u32 ) -> bool {
513+ let key = string_value ( & index. to_string ( ) ) ;
514+ crate :: object:: js_object_has_property ( obj, key) . to_bits ( ) == crate :: value:: TAG_TRUE
515+ }
516+
509517/// CanonicalizeLocaleList element handler: a present element must be a String or
510518/// an Object (an `Intl.Locale` or anything ToString-able), else `TypeError`; the
511519/// resulting tag is canonicalized (`RangeError` if structurally invalid) and
@@ -520,7 +528,7 @@ fn push_locale_element(out: &mut Vec<String>, value: f64) {
520528 // undefined / null / boolean / number / Symbol element → TypeError.
521529 throw_type_error ( "locale must be a String or Object" ) ;
522530 } ;
523- let Some ( canonical) = canonical_locale ( & tag) else {
531+ let Some ( canonical) = canonicalize_language_tag ( & tag) else {
524532 throw_invalid_language_tag ( & tag) ;
525533 } ;
526534 if !out. iter ( ) . any ( |existing| existing == & canonical) {
@@ -541,7 +549,7 @@ fn locales_from_value(locales: f64) -> Vec<String> {
541549 // A String argument is treated as a single-element list (not iterated by char).
542550 if js. is_any_string ( ) {
543551 let tag = string_from_string_value ( locales) . unwrap_or_default ( ) ;
544- let Some ( canonical) = canonical_locale ( & tag) else {
552+ let Some ( canonical) = canonicalize_language_tag ( & tag) else {
545553 throw_invalid_language_tag ( & tag) ;
546554 } ;
547555 return vec ! [ canonical] ;
@@ -568,6 +576,11 @@ fn locales_from_value(locales: f64) -> Vec<String> {
568576 } ;
569577 let mut out = Vec :: with_capacity ( len as usize ) ;
570578 for i in 0 ..len {
579+ // Skip absent indices (`HasProperty` is false) — e.g.
580+ // `{ length: 3, 0: "en" }` yields just `["en"]`, never `undefined`.
581+ if !js_has_index ( locales, i) {
582+ continue ;
583+ }
571584 push_locale_element ( & mut out, get_field ( obj, & i. to_string ( ) ) ) ;
572585 }
573586 return out;
@@ -611,9 +624,14 @@ fn unicode_extension_keyword(locale: &str, key: &str) -> Option<String> {
611624 let lower = locale. to_ascii_lowercase ( ) ;
612625 let key = key. to_ascii_lowercase ( ) ;
613626 let mut iter = lower. split ( '-' ) ;
614- // Advance to the `u` singleton.
627+ // Advance to the `u` singleton. A `x` singleton starts the private-use
628+ // sequence (which must come last); a `u` inside it — e.g. `en-x-u-kn` — is
629+ // private data, not a Unicode extension, so stop scanning there.
615630 let mut in_u = false ;
616- while let Some ( p) = iter. next ( ) {
631+ for p in iter. by_ref ( ) {
632+ if p == "x" {
633+ return None ;
634+ }
617635 if p == "u" {
618636 in_u = true ;
619637 break ;
@@ -1202,27 +1220,34 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
12021220 // (constructor-options-throwing-getters / resolvedOptions order.js).
12031221 let options = coerce_options_reject_null ( options) ;
12041222 let usage = enum_option_strict ( options, "usage" , & [ "sort" , "search" ] , "sort" ) ;
1205- let _ = enum_option_strict ( options, "localeMatcher" , & [ "lookup" , "best fit" ] , "best fit" ) ;
1206- // `collation` is a free-form `type` string (RangeError if malformed);
1207- // it has no CLDR effect here, so it resolves to "default".
1208- if let Some ( collation) = get_option_string_coerced ( options, "collation" ) {
1209- if !is_well_formed_numbering_system ( & collation) {
1223+ let _ = enum_option_strict (
1224+ options,
1225+ "localeMatcher" ,
1226+ & [ "lookup" , "best fit" ] ,
1227+ "best fit" ,
1228+ ) ;
1229+ // `collation` is a `type` string: malformed, or the reserved `standard`
1230+ // /`search` values, are a RangeError (the latter are only valid as a
1231+ // `usage` selector, never an explicit collation). A valid value wins
1232+ // over any `-u-co-` keyword; absent ⇒ fall back to the extension.
1233+ let collation_opt = get_option_string_coerced ( options, "collation" ) . map ( |v| {
1234+ if !is_well_formed_numbering_system ( & v) || v == "standard" || v == "search" {
12101235 throw_range_error ( & format ! (
1211- "Value {collation } out of range for Intl options property collation"
1236+ "Value {v } out of range for Intl options property collation"
12121237 ) ) ;
12131238 }
1214- }
1239+ v
1240+ } ) ;
12151241 let numeric_opt = get_bool_option ( options, "numeric" ) ;
1216- let case_first_opt =
1217- get_option_string_coerced ( options, "caseFirst" ) . map ( |v| {
1218- if [ "upper" , "lower" , "false" ] . contains ( & v. as_str ( ) ) {
1219- v
1220- } else {
1221- throw_range_error ( & format ! (
1222- "Value {v} out of range for Intl options property caseFirst"
1223- ) )
1224- }
1225- } ) ;
1242+ let case_first_opt = get_option_string_coerced ( options, "caseFirst" ) . map ( |v| {
1243+ if [ "upper" , "lower" , "false" ] . contains ( & v. as_str ( ) ) {
1244+ v
1245+ } else {
1246+ throw_range_error ( & format ! (
1247+ "Value {v} out of range for Intl options property caseFirst"
1248+ ) )
1249+ }
1250+ } ) ;
12261251 let sensitivity = enum_option_strict (
12271252 options,
12281253 "sensitivity" ,
@@ -1233,20 +1258,21 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
12331258 // ResolveLocale: when an option is absent, fall back to the matching
12341259 // Unicode (`-u-`) extension keyword in the resolved locale — `kn`
12351260 // (numeric, value-less ⇒ true) and `kf` (caseFirst).
1236- let numeric = numeric_opt . unwrap_or_else ( || {
1237- match unicode_extension_keyword ( & locale, "kn" ) {
1261+ let numeric =
1262+ numeric_opt . unwrap_or_else ( || match unicode_extension_keyword ( & locale, "kn" ) {
12381263 Some ( v) => v != "false" ,
12391264 None => false ,
1240- }
1241- } ) ;
1265+ } ) ;
12421266 let case_first = case_first_opt. unwrap_or_else ( || {
12431267 unicode_extension_keyword ( & locale, "kf" )
12441268 . filter ( |v| [ "upper" , "lower" , "false" ] . contains ( & v. as_str ( ) ) )
12451269 . unwrap_or_else ( || "false" . to_string ( ) )
12461270 } ) ;
1247- let collation = unicode_extension_keyword ( & locale, "co" )
1248- . filter ( |v| !v. is_empty ( ) && v != "standard" && v != "search" )
1249- . unwrap_or_else ( || "default" . to_string ( ) ) ;
1271+ let collation = collation_opt. unwrap_or_else ( || {
1272+ unicode_extension_keyword ( & locale, "co" )
1273+ . filter ( |v| !v. is_empty ( ) && v != "standard" && v != "search" )
1274+ . unwrap_or_else ( || "default" . to_string ( ) )
1275+ } ) ;
12501276 set_internal_field ( obj, KEY_COL_USAGE , string_value ( & usage) ) ;
12511277 set_internal_field ( obj, KEY_COL_SENSITIVITY , string_value ( & sensitivity) ) ;
12521278 set_internal_field ( obj, KEY_COL_IGNORE_PUNCT , bool_value ( ignore_punct) ) ;
@@ -1270,7 +1296,12 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
12701296 // `? ToObject(options)` (null → TypeError), then GetOption in order:
12711297 // localeMatcher, granularity (options-order.js / options-null.js).
12721298 let options = coerce_options_reject_null ( options) ;
1273- let _ = enum_option_strict ( options, "localeMatcher" , & [ "lookup" , "best fit" ] , "best fit" ) ;
1299+ let _ = enum_option_strict (
1300+ options,
1301+ "localeMatcher" ,
1302+ & [ "lookup" , "best fit" ] ,
1303+ "best fit" ,
1304+ ) ;
12741305 let granularity =
12751306 normalize_granularity ( get_option_string_coerced ( options, "granularity" ) ) ;
12761307 set_internal_field ( obj, KEY_GRANULARITY , string_value ( & granularity) ) ;
@@ -1292,7 +1323,12 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
12921323 // TypeError), then GetOption: localeMatcher, type, style
12931324 // (options-getoptionsobject.js / options-order.js).
12941325 let options = get_options_object ( options) ;
1295- let _ = enum_option_strict ( options, "localeMatcher" , & [ "lookup" , "best fit" ] , "best fit" ) ;
1326+ let _ = enum_option_strict (
1327+ options,
1328+ "localeMatcher" ,
1329+ & [ "lookup" , "best fit" ] ,
1330+ "best fit" ,
1331+ ) ;
12961332 let list_type = enum_option_strict (
12971333 options,
12981334 "type" ,
@@ -1325,7 +1361,12 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
13251361 // `? ToObject(options)` (null → TypeError), then GetOption in order:
13261362 // localeMatcher, numberingSystem, style, numeric (options-order.js).
13271363 let options = coerce_options_reject_null ( options) ;
1328- let _ = enum_option_strict ( options, "localeMatcher" , & [ "lookup" , "best fit" ] , "best fit" ) ;
1364+ let _ = enum_option_strict (
1365+ options,
1366+ "localeMatcher" ,
1367+ & [ "lookup" , "best fit" ] ,
1368+ "best fit" ,
1369+ ) ;
13291370 if let Some ( ns) = get_option_string_coerced ( options, "numberingSystem" ) {
13301371 if !is_well_formed_numbering_system ( & ns) {
13311372 throw_range_error ( & format ! (
@@ -1358,7 +1399,12 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
13581399 // (minimumIntegerDigits, min/maxFractionDigits, min/maxSignificantDigits,
13591400 // roundingIncrement, roundingMode, roundingPriority, trailingZeroDisplay).
13601401 let options = get_options_object ( options) ;
1361- let _ = enum_option_strict ( options, "localeMatcher" , & [ "lookup" , "best fit" ] , "best fit" ) ;
1402+ let _ = enum_option_strict (
1403+ options,
1404+ "localeMatcher" ,
1405+ & [ "lookup" , "best fit" ] ,
1406+ "best fit" ,
1407+ ) ;
13621408 let pr_type = enum_option_strict ( options, "type" , & [ "cardinal" , "ordinal" ] , "cardinal" ) ;
13631409 set_internal_field ( obj, KEY_TYPE , string_value ( & pr_type) ) ;
13641410 let notation = enum_option_strict (
@@ -1514,17 +1560,26 @@ extern "C" fn plural_rules_constructor_thunk(closure: *const ClosureHeader, rest
15141560}
15151561
15161562fn supported_locales_array ( locales : f64 , options : f64 ) -> f64 {
1517- // SupportedLocales: when `options` is not undefined, `? ToObject(options)`
1518- // (null → TypeError) then `? GetOption(options, "localeMatcher", …)` — so an
1519- // invalid localeMatcher is a RangeError even though the matcher choice does
1520- // not affect Perry's lookup result.
1563+ // `supportedLocalesOf(locales, options)`:
1564+ // 1. requestedLocales = ? CanonicalizeLocaleList(locales) ← runs FIRST,
1565+ // so a malformed locale errors before `options` is touched.
1566+ // 2. SupportedLocales(..., options): when `options` is not undefined,
1567+ // `? ToObject(options)` (null → TypeError) then
1568+ // `? GetOption(options, "localeMatcher", …)` — an invalid localeMatcher
1569+ // is a RangeError even though the matcher choice does not affect Perry's
1570+ // lookup result.
1571+ let requested = locales_from_value ( locales) ;
15211572 if !JSValue :: from_bits ( options. to_bits ( ) ) . is_undefined ( ) {
15221573 let options = coerce_options_reject_null ( options) ;
1523- let _ = enum_option_strict ( options, "localeMatcher" , & [ "lookup" , "best fit" ] , "best fit" ) ;
1574+ let _ = enum_option_strict (
1575+ options,
1576+ "localeMatcher" ,
1577+ & [ "lookup" , "best fit" ] ,
1578+ "best fit" ,
1579+ ) ;
15241580 }
15251581 // BestAvailableLocale-filter the canonicalized request list: drop tags whose
15261582 // primary language Perry can't service (e.g. `zxx`), keeping order + dedup.
1527- let requested = locales_from_value ( locales) ;
15281583 let mut arr = js_array_alloc ( 0 ) ;
15291584 for locale in requested {
15301585 if is_available_locale ( & locale) {
0 commit comments