From 3518685dae9b85545c8cc6ded7df02b57db574f8 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 26 Jul 2026 00:47:53 +0000 Subject: [PATCH] Stdlib: IntlListFormatter on PROFILE=8.5 (#23229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PHP-in-PHP IntlListFormatter via ICU ulistfmt_* FFI, gated on host php-intl and language profile ≥8.5, matching php-src listformatter. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 538 +++++++------- ext/intl/BuiltinClasses.php | 22 +- ext/intl/IntlExtensionPolicy.php | 11 + ext/intl/IntlSerializeDeny.php | 2 + ext/intl/Module.php | 3 + ext/intl/VmIntlListFormatter.php | 656 ++++++++++++++++++ lib/CompilerVersion.php | 11 + test/compliance/JITTest.php | 5 + test/compliance/VMTest.php | 1 + .../cases/intl/intl_list_formatter_85.phpt | 51 ++ .../intl/intl_list_formatter_profile82.phpt | 20 + .../cases/stdlib/intl_phantom_classes.phpt | 2 + .../maintainer_gap_intl_list_formatter_85.php | 31 + ...ntainer_gap_intl_list_formatter_absent.php | 19 + .../compiler_lib_spine_smoke/main.php | 1 + test/unit/VmIntlListFormatterTest.php | 79 +++ 16 files changed, 1190 insertions(+), 262 deletions(-) create mode 100644 ext/intl/VmIntlListFormatter.php create mode 100644 test/compliance/cases/intl/intl_list_formatter_85.phpt create mode 100644 test/compliance/cases/intl/intl_list_formatter_profile82.phpt create mode 100644 test/repro/maintainer_gap_intl_list_formatter_85.php create mode 100644 test/repro/maintainer_gap_intl_list_formatter_absent.php create mode 100644 test/unit/VmIntlListFormatterTest.php diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index cd87308f08..3ea2daa94d 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,11 +8,11 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 6561 | -| Phase A inventory files (M2 ratio SSOT) | 6561 | +| PHP files on vm.php path | 6562 | +| Phase A inventory files (M2 ratio SSOT) | 6562 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 21094 | +| Source constructs flagged (warnings) | 21111 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -968,6 +968,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/intl/VmIntlDateFormatter.php` | 0 | 25 | | `ext/intl/VmIntlDatePatternGenerator.php` | 0 | 12 | | `ext/intl/VmIntlIterator.php` | 0 | 10 | +| `ext/intl/VmIntlListFormatter.php` | 0 | 17 | | `ext/intl/VmIntlTimeZone.php` | 0 | 63 | | `ext/intl/VmLocale.php` | 0 | 2 | | `ext/intl/VmMessageFormatter.php` | 0 | 37 | @@ -12944,59 +12945,59 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/intl/BuiltinClasses.php` **Warnings** (review for bootstrap subset): -- new ClassEntry (line 86) -- new NormalizerNormalize (line 97) -- new NormalizerIsNormalized (line 98) -- new NormalizerGetRawDecomposition (line 99) -- new ClassEntry (line 115) -- new LocaleGetDefault (line 118) -- new LocaleSetDefault (line 119) -- new LocaleGetPrimaryLanguage (line 120) -- new LocaleGetRegion (line 121) -- new LocaleGetScript (line 122) -- new LocaleGetDisplayName (line 123) -- new LocaleGetDisplayLanguage (line 124) -- new LocaleGetDisplayRegion (line 125) -- new LocaleGetDisplayScript (line 126) -- new LocaleGetDisplayVariant (line 127) -- new LocaleGetAllVariants (line 128) -- new LocaleLookup (line 129) -- new LocaleFilterMatches (line 130) -- new LocaleAcceptFromHttp (line 131) -- new LocaleCanonicalize (line 132) -- new LocaleParseLocale (line 133) -- new LocaleComposeLocale (line 134) -- new LocaleGetKeywords (line 135) -- new LocaleIsRightToLeft (line 138) -- new LocaleAddLikelySubtags (line 139) -- new LocaleMinimizeSubtags (line 140) -- new LocaleGetDisplayKeyword (line 143) -- new LocaleGetDisplayKeywordValue (line 144) -- new ClassEntry (line 160) -- new IntlDateFormatterConstruct (line 172) -- new IntlDateFormatterCreate (line 177) -- new IntlDateFormatterFormat (line 180) -- new IntlDateFormatterFormatObject (line 183) -- new IntlDateFormatterGetPattern (line 186) -- new IntlDateFormatterSetPattern (line 189) -- new IntlDateFormatterGetLocale (line 192) -- new IntlDateFormatterGetDateType (line 195) -- new IntlDateFormatterGetTimeType (line 198) -- new IntlDateFormatterIsLenient (line 201) -- new IntlDateFormatterSetLenient (line 204) -- new IntlDateFormatterGetCalendar (line 207) -- new IntlDateFormatterSetCalendar (line 210) -- new IntlDateFormatterGetTimeZoneId (line 213) -- new IntlDateFormatterGetCalendarObject (line 216) -- new IntlDateFormatterParse (line 219) -- new IntlDateFormatterParseToCalendar (line 222) -- new IntlDateFormatterLocaltime (line 225) -- new IntlDateFormatterGetTimeZone (line 228) -- new IntlDateFormatterSetTimeZone (line 231) -- new IntlDateFormatterGetErrorCode (line 234) -- new IntlDateFormatterGetErrorMessage (line 237) -- new ClassEntry (line 326) -- 17 class method(s) +- new ClassEntry (line 89) +- new NormalizerNormalize (line 100) +- new NormalizerIsNormalized (line 101) +- new NormalizerGetRawDecomposition (line 102) +- new ClassEntry (line 118) +- new LocaleGetDefault (line 121) +- new LocaleSetDefault (line 122) +- new LocaleGetPrimaryLanguage (line 123) +- new LocaleGetRegion (line 124) +- new LocaleGetScript (line 125) +- new LocaleGetDisplayName (line 126) +- new LocaleGetDisplayLanguage (line 127) +- new LocaleGetDisplayRegion (line 128) +- new LocaleGetDisplayScript (line 129) +- new LocaleGetDisplayVariant (line 130) +- new LocaleGetAllVariants (line 131) +- new LocaleLookup (line 132) +- new LocaleFilterMatches (line 133) +- new LocaleAcceptFromHttp (line 134) +- new LocaleCanonicalize (line 135) +- new LocaleParseLocale (line 136) +- new LocaleComposeLocale (line 137) +- new LocaleGetKeywords (line 138) +- new LocaleIsRightToLeft (line 141) +- new LocaleAddLikelySubtags (line 142) +- new LocaleMinimizeSubtags (line 143) +- new LocaleGetDisplayKeyword (line 146) +- new LocaleGetDisplayKeywordValue (line 147) +- new ClassEntry (line 163) +- new IntlDateFormatterConstruct (line 175) +- new IntlDateFormatterCreate (line 180) +- new IntlDateFormatterFormat (line 183) +- new IntlDateFormatterFormatObject (line 186) +- new IntlDateFormatterGetPattern (line 189) +- new IntlDateFormatterSetPattern (line 192) +- new IntlDateFormatterGetLocale (line 195) +- new IntlDateFormatterGetDateType (line 198) +- new IntlDateFormatterGetTimeType (line 201) +- new IntlDateFormatterIsLenient (line 204) +- new IntlDateFormatterSetLenient (line 207) +- new IntlDateFormatterGetCalendar (line 210) +- new IntlDateFormatterSetCalendar (line 213) +- new IntlDateFormatterGetTimeZoneId (line 216) +- new IntlDateFormatterGetCalendarObject (line 219) +- new IntlDateFormatterParse (line 222) +- new IntlDateFormatterParseToCalendar (line 225) +- new IntlDateFormatterLocaltime (line 228) +- new IntlDateFormatterGetTimeZone (line 231) +- new IntlDateFormatterSetTimeZone (line 234) +- new IntlDateFormatterGetErrorCode (line 237) +- new IntlDateFormatterGetErrorMessage (line 240) +- new ClassEntry (line 340) +- 18 class method(s) ### `ext/intl/GraphemeStrSplitJitHelper.php` @@ -13115,16 +13116,16 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/intl/IntlExtensionPolicy.php` **Warnings** (review for bootstrap subset): -- new locale_get_primary_language (line 323) -- new locale_get_region (line 324) -- new locale_get_script (line 325) -- 30 class method(s) +- new locale_get_primary_language (line 334) +- new locale_get_region (line 335) +- new locale_get_script (line 336) +- 31 class method(s) ### `ext/intl/IntlSerializeDeny.php` **Warnings** (review for bootstrap subset): -- new Exception (line 47) -- new Exception (line 54) +- new Exception (line 49) +- new Exception (line 56) - 5 class method(s) ### `ext/intl/JitGrapheme.php` @@ -13305,201 +13306,201 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/intl/Module.php` **Warnings** (review for bootstrap subset): -- new VM\Variable (line 79) -- new locale_get_default (line 89) -- new locale_set_default (line 90) -- new locale_get_primary_language (line 91) -- new locale_get_region (line 92) -- new locale_get_script (line 93) -- new locale_lookup (line 94) -- new locale_filter_matches (line 95) -- new locale_accept_from_http (line 96) -- new locale_canonicalize (line 97) -- new locale_parse (line 98) -- new locale_compose (line 99) -- new locale_get_keywords (line 100) -- new locale_get_display_language (line 101) -- new locale_get_display_name (line 102) -- new locale_get_display_region (line 103) -- new locale_get_display_script (line 104) -- new locale_get_display_variant (line 105) -- new locale_get_all_variants (line 106) -- new locale_is_right_to_left (line 108) -- new locale_add_likely_subtags (line 109) -- new locale_minimize_subtags (line 110) -- new locale_get_display_keyword (line 113) -- new locale_get_display_keyword_value (line 114) -- new normalizer_normalize (line 119) -- new normalizer_is_normalized (line 120) -- new normalizer_get_raw_decomposition (line 121) -- new idn_to_ascii (line 125) -- new idn_to_utf8 (line 125) -- new collator_create (line 130) -- new collator_compare (line 131) -- new collator_sort (line 132) -- new collator_asort (line 133) -- new collator_sort_with_sort_keys (line 134) -- new collator_get_attribute (line 135) -- new collator_set_attribute (line 136) -- new collator_get_strength (line 137) -- new collator_set_strength (line 138) -- new collator_get_sort_key (line 139) -- new collator_get_locale (line 140) -- new collator_get_error_code (line 141) -- new collator_get_error_message (line 142) -- new numfmt_create (line 148) -- new numfmt_format (line 149) -- new numfmt_parse (line 150) -- new numfmt_parse_currency (line 151) -- new numfmt_format_currency (line 152) -- new numfmt_get_attribute (line 153) -- new numfmt_set_attribute (line 154) -- new numfmt_get_symbol (line 155) -- new numfmt_set_symbol (line 156) -- new numfmt_get_text_attribute (line 157) -- new numfmt_set_text_attribute (line 158) -- new numfmt_get_pattern (line 159) -- new numfmt_set_pattern (line 160) -- new numfmt_get_locale (line 161) -- new numfmt_get_error_code (line 162) -- new numfmt_get_error_message (line 163) -- new msgfmt_create (line 169) -- new msgfmt_format (line 170) -- new msgfmt_format_message (line 171) -- new msgfmt_parse (line 172) -- new msgfmt_parse_message (line 173) -- new msgfmt_get_locale (line 174) -- new msgfmt_get_pattern (line 175) -- new msgfmt_set_pattern (line 176) -- new msgfmt_get_error_code (line 177) -- new msgfmt_get_error_message (line 178) -- new transliterator_create (line 184) -- new transliterator_create_from_rules (line 185) -- new transliterator_create_inverse (line 186) -- new transliterator_list_ids (line 187) -- new transliterator_transliterate (line 188) -- new transliterator_get_error_code (line 189) -- new transliterator_get_error_message (line 190) -- new resourcebundle_create (line 196) -- new resourcebundle_get (line 197) -- new resourcebundle_locales (line 198) -- new resourcebundle_count (line 199) -- new resourcebundle_get_error_code (line 200) -- new resourcebundle_get_error_message (line 201) -- new datefmt_create (line 207) -- new datefmt_format (line 208) -- new datefmt_format_object (line 209) -- new datefmt_parse (line 210) -- new datefmt_localtime (line 211) -- new datefmt_get_error_code (line 212) -- new datefmt_get_error_message (line 213) -- new datefmt_get_pattern (line 214) -- new datefmt_set_pattern (line 215) -- new datefmt_get_timezone (line 216) -- new datefmt_set_timezone (line 217) -- new datefmt_get_locale (line 218) -- new datefmt_get_datetype (line 219) -- new datefmt_get_timetype (line 220) -- new datefmt_is_lenient (line 221) -- new datefmt_set_lenient (line 222) -- new datefmt_get_calendar (line 223) -- new datefmt_set_calendar (line 224) -- new datefmt_get_timezone_id (line 225) -- new datefmt_get_calendar_object (line 226) -- new intlcal_create_instance (line 232) -- new intlcal_get_now (line 233) -- new intlcal_from_date_time (line 234) -- new intlcal_get (line 235) -- new intlcal_set (line 236) -- new intlcal_get_type (line 237) -- new intlcal_add (line 238) -- new intlcal_roll (line 239) -- new intlcal_clear (line 240) -- new intlcal_is_set (line 241) -- new intlcal_equals (line 242) -- new intlcal_get_time (line 243) -- new intlcal_set_time (line 244) -- new intlcal_get_time_zone (line 245) -- new intlcal_to_date_time (line 246) -- new intlcal_field_difference (line 247) -- new intlcal_before (line 248) -- new intlcal_after (line 249) -- new intlcal_set_time_zone (line 250) -- new intlcal_get_minimum (line 251) -- new intlcal_get_maximum (line 252) -- new intlcal_get_available_locales (line 253) -- new intlcal_is_weekend (line 255) -- new intlcal_get_actual_minimum (line 256) -- new intlcal_get_actual_maximum (line 257) -- new intlcal_get_least_maximum (line 258) -- new intlcal_get_greatest_minimum (line 259) -- new intlcal_get_day_of_week_type (line 260) -- new intlcal_get_weekend_transition (line 261) -- new intlcal_get_repeated_wall_time_option (line 262) -- new intlcal_set_repeated_wall_time_option (line 263) -- new intlcal_get_skipped_wall_time_option (line 264) -- new intlcal_set_skipped_wall_time_option (line 265) -- new intlcal_get_error_code (line 266) -- new intlcal_get_error_message (line 267) -- new intlcal_get_locale (line 269) -- new intlcal_is_lenient (line 270) -- new intlcal_set_lenient (line 271) -- new intlcal_in_daylight_time (line 272) -- new intlcal_get_first_day_of_week (line 273) -- new intlcal_set_first_day_of_week (line 274) -- new intlcal_get_minimal_days_in_first_week (line 275) -- new intlcal_set_minimal_days_in_first_week (line 276) -- new intlcal_get_keyword_values_for_locale (line 277) -- new intlcal_is_equivalent_to (line 278) -- new intlgregcal_create_instance (line 280) -- new intlgregcal_is_leap_year (line 281) -- new intlgregcal_get_gregorian_change (line 282) -- new intlgregcal_set_gregorian_change (line 283) -- new intlgregcal_create_from_date (line 284) -- new intlgregcal_create_from_date_time (line 285) -- new intltz_get_gmt (line 287) -- new intltz_create_time_zone (line 288) -- new intltz_create_default (line 289) -- new intltz_get_id (line 290) -- new intltz_get_display_name (line 291) -- new intltz_get_raw_offset (line 292) -- new intltz_get_dst_savings (line 293) -- new intltz_from_date_time_zone (line 294) -- new intltz_to_date_time_zone (line 295) -- new intltz_get_canonical_id (line 296) -- new intltz_get_region (line 297) -- new intltz_count_equivalent_ids (line 299) -- new intltz_get_equivalent_id (line 300) -- new intltz_get_windows_id (line 301) -- new intltz_get_id_for_windows_id (line 302) -- new intltz_create_enumeration (line 303) -- new intltz_create_time_zone_id_enumeration (line 304) -- new intltz_get_unknown (line 305) -- new intltz_get_utc (line 306) -- new intltz_get_tz_data_version (line 307) -- new intltz_use_daylight_time (line 308) -- new intltz_has_same_rules (line 309) -- new intltz_get_error_code (line 310) -- new intltz_get_error_message (line 311) -- new intltz_get_offset (line 312) -- new intltz_get_iana_id (line 317) -- new grapheme_strlen (line 347) -- new grapheme_substr (line 348) -- new grapheme_strpos (line 349) -- new grapheme_str_contains (line 351) -- new grapheme_strstr (line 352) -- new grapheme_stristr (line 353) -- new grapheme_stripos (line 354) -- new grapheme_strrpos (line 355) -- new grapheme_strripos (line 356) -- new grapheme_extract (line 357) -- new grapheme_levenshtein (line 359) -- new grapheme_str_split (line 360) -- new grapheme_strimwidth (line 361) -- new intl_get_error_code (line 362) -- new intl_get_error_message (line 363) -- new intl_is_failure (line 364) -- new intl_error_name (line 365) +- new VM\Variable (line 82) +- new locale_get_default (line 92) +- new locale_set_default (line 93) +- new locale_get_primary_language (line 94) +- new locale_get_region (line 95) +- new locale_get_script (line 96) +- new locale_lookup (line 97) +- new locale_filter_matches (line 98) +- new locale_accept_from_http (line 99) +- new locale_canonicalize (line 100) +- new locale_parse (line 101) +- new locale_compose (line 102) +- new locale_get_keywords (line 103) +- new locale_get_display_language (line 104) +- new locale_get_display_name (line 105) +- new locale_get_display_region (line 106) +- new locale_get_display_script (line 107) +- new locale_get_display_variant (line 108) +- new locale_get_all_variants (line 109) +- new locale_is_right_to_left (line 111) +- new locale_add_likely_subtags (line 112) +- new locale_minimize_subtags (line 113) +- new locale_get_display_keyword (line 116) +- new locale_get_display_keyword_value (line 117) +- new normalizer_normalize (line 122) +- new normalizer_is_normalized (line 123) +- new normalizer_get_raw_decomposition (line 124) +- new idn_to_ascii (line 128) +- new idn_to_utf8 (line 128) +- new collator_create (line 133) +- new collator_compare (line 134) +- new collator_sort (line 135) +- new collator_asort (line 136) +- new collator_sort_with_sort_keys (line 137) +- new collator_get_attribute (line 138) +- new collator_set_attribute (line 139) +- new collator_get_strength (line 140) +- new collator_set_strength (line 141) +- new collator_get_sort_key (line 142) +- new collator_get_locale (line 143) +- new collator_get_error_code (line 144) +- new collator_get_error_message (line 145) +- new numfmt_create (line 151) +- new numfmt_format (line 152) +- new numfmt_parse (line 153) +- new numfmt_parse_currency (line 154) +- new numfmt_format_currency (line 155) +- new numfmt_get_attribute (line 156) +- new numfmt_set_attribute (line 157) +- new numfmt_get_symbol (line 158) +- new numfmt_set_symbol (line 159) +- new numfmt_get_text_attribute (line 160) +- new numfmt_set_text_attribute (line 161) +- new numfmt_get_pattern (line 162) +- new numfmt_set_pattern (line 163) +- new numfmt_get_locale (line 164) +- new numfmt_get_error_code (line 165) +- new numfmt_get_error_message (line 166) +- new msgfmt_create (line 172) +- new msgfmt_format (line 173) +- new msgfmt_format_message (line 174) +- new msgfmt_parse (line 175) +- new msgfmt_parse_message (line 176) +- new msgfmt_get_locale (line 177) +- new msgfmt_get_pattern (line 178) +- new msgfmt_set_pattern (line 179) +- new msgfmt_get_error_code (line 180) +- new msgfmt_get_error_message (line 181) +- new transliterator_create (line 187) +- new transliterator_create_from_rules (line 188) +- new transliterator_create_inverse (line 189) +- new transliterator_list_ids (line 190) +- new transliterator_transliterate (line 191) +- new transliterator_get_error_code (line 192) +- new transliterator_get_error_message (line 193) +- new resourcebundle_create (line 199) +- new resourcebundle_get (line 200) +- new resourcebundle_locales (line 201) +- new resourcebundle_count (line 202) +- new resourcebundle_get_error_code (line 203) +- new resourcebundle_get_error_message (line 204) +- new datefmt_create (line 210) +- new datefmt_format (line 211) +- new datefmt_format_object (line 212) +- new datefmt_parse (line 213) +- new datefmt_localtime (line 214) +- new datefmt_get_error_code (line 215) +- new datefmt_get_error_message (line 216) +- new datefmt_get_pattern (line 217) +- new datefmt_set_pattern (line 218) +- new datefmt_get_timezone (line 219) +- new datefmt_set_timezone (line 220) +- new datefmt_get_locale (line 221) +- new datefmt_get_datetype (line 222) +- new datefmt_get_timetype (line 223) +- new datefmt_is_lenient (line 224) +- new datefmt_set_lenient (line 225) +- new datefmt_get_calendar (line 226) +- new datefmt_set_calendar (line 227) +- new datefmt_get_timezone_id (line 228) +- new datefmt_get_calendar_object (line 229) +- new intlcal_create_instance (line 235) +- new intlcal_get_now (line 236) +- new intlcal_from_date_time (line 237) +- new intlcal_get (line 238) +- new intlcal_set (line 239) +- new intlcal_get_type (line 240) +- new intlcal_add (line 241) +- new intlcal_roll (line 242) +- new intlcal_clear (line 243) +- new intlcal_is_set (line 244) +- new intlcal_equals (line 245) +- new intlcal_get_time (line 246) +- new intlcal_set_time (line 247) +- new intlcal_get_time_zone (line 248) +- new intlcal_to_date_time (line 249) +- new intlcal_field_difference (line 250) +- new intlcal_before (line 251) +- new intlcal_after (line 252) +- new intlcal_set_time_zone (line 253) +- new intlcal_get_minimum (line 254) +- new intlcal_get_maximum (line 255) +- new intlcal_get_available_locales (line 256) +- new intlcal_is_weekend (line 258) +- new intlcal_get_actual_minimum (line 259) +- new intlcal_get_actual_maximum (line 260) +- new intlcal_get_least_maximum (line 261) +- new intlcal_get_greatest_minimum (line 262) +- new intlcal_get_day_of_week_type (line 263) +- new intlcal_get_weekend_transition (line 264) +- new intlcal_get_repeated_wall_time_option (line 265) +- new intlcal_set_repeated_wall_time_option (line 266) +- new intlcal_get_skipped_wall_time_option (line 267) +- new intlcal_set_skipped_wall_time_option (line 268) +- new intlcal_get_error_code (line 269) +- new intlcal_get_error_message (line 270) +- new intlcal_get_locale (line 272) +- new intlcal_is_lenient (line 273) +- new intlcal_set_lenient (line 274) +- new intlcal_in_daylight_time (line 275) +- new intlcal_get_first_day_of_week (line 276) +- new intlcal_set_first_day_of_week (line 277) +- new intlcal_get_minimal_days_in_first_week (line 278) +- new intlcal_set_minimal_days_in_first_week (line 279) +- new intlcal_get_keyword_values_for_locale (line 280) +- new intlcal_is_equivalent_to (line 281) +- new intlgregcal_create_instance (line 283) +- new intlgregcal_is_leap_year (line 284) +- new intlgregcal_get_gregorian_change (line 285) +- new intlgregcal_set_gregorian_change (line 286) +- new intlgregcal_create_from_date (line 287) +- new intlgregcal_create_from_date_time (line 288) +- new intltz_get_gmt (line 290) +- new intltz_create_time_zone (line 291) +- new intltz_create_default (line 292) +- new intltz_get_id (line 293) +- new intltz_get_display_name (line 294) +- new intltz_get_raw_offset (line 295) +- new intltz_get_dst_savings (line 296) +- new intltz_from_date_time_zone (line 297) +- new intltz_to_date_time_zone (line 298) +- new intltz_get_canonical_id (line 299) +- new intltz_get_region (line 300) +- new intltz_count_equivalent_ids (line 302) +- new intltz_get_equivalent_id (line 303) +- new intltz_get_windows_id (line 304) +- new intltz_get_id_for_windows_id (line 305) +- new intltz_create_enumeration (line 306) +- new intltz_create_time_zone_id_enumeration (line 307) +- new intltz_get_unknown (line 308) +- new intltz_get_utc (line 309) +- new intltz_get_tz_data_version (line 310) +- new intltz_use_daylight_time (line 311) +- new intltz_has_same_rules (line 312) +- new intltz_get_error_code (line 313) +- new intltz_get_error_message (line 314) +- new intltz_get_offset (line 315) +- new intltz_get_iana_id (line 320) +- new grapheme_strlen (line 350) +- new grapheme_substr (line 351) +- new grapheme_strpos (line 352) +- new grapheme_str_contains (line 354) +- new grapheme_strstr (line 355) +- new grapheme_stristr (line 356) +- new grapheme_stripos (line 357) +- new grapheme_strrpos (line 358) +- new grapheme_strripos (line 359) +- new grapheme_extract (line 360) +- new grapheme_levenshtein (line 362) +- new grapheme_str_split (line 363) +- new grapheme_strimwidth (line 364) +- new intl_get_error_code (line 365) +- new intl_get_error_message (line 366) +- new intl_is_failure (line 367) +- new intl_error_name (line 368) - 4 class method(s) ### `ext/intl/NormalizerGetRawDecomposition.php` @@ -14031,6 +14032,27 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new ObjectEntry (line 72) - 17 class method(s) +### `ext/intl/VmIntlListFormatter.php` + +**Warnings** (review for bootstrap subset): +- new ClassEntry (line 66) +- new Variable (line 71) +- new IntlListFormatterConstruct (line 77) +- new IntlListFormatterFormat (line 81) +- new IntlListFormatterGetErrorCode (line 82) +- new IntlListFormatterGetErrorMessage (line 83) +- new Error (line 132) +- new IntlException (line 179) +- new ArgumentCountError (line 524) +- new Error (line 532) +- new ArgumentCountError (line 573) +- new Error (line 581) +- new ArgumentCountError (line 614) +- new Error (line 622) +- new ArgumentCountError (line 642) +- new Error (line 650) +- 23 class method(s) + ### `ext/intl/VmIntlTimeZone.php` **Warnings** (review for bootstrap subset): @@ -41974,7 +41996,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/CompilerVersion.php` **Warnings** (review for bootstrap subset): -- 258 class method(s) +- 259 class method(s) ### `lib/CurlyBraceOffsetRejector.php` diff --git a/ext/intl/BuiltinClasses.php b/ext/intl/BuiltinClasses.php index f923a3af12..5408e3c113 100644 --- a/ext/intl/BuiltinClasses.php +++ b/ext/intl/BuiltinClasses.php @@ -12,10 +12,10 @@ * Register intl builtin classes (php-src ext/intl/php_intl.c; issues #5774, #6696, #19549, #6151, #19670). * * Locale / IntlDateFormatter / IntlDatePatternGenerator / IntlCalendar / IntlTimeZone / - * NumberFormatter / Normalizer / Collator / MessageFormatter / Transliterator / ResourceBundle / - * IntlBreakIterator / IntlCodePointBreakIterator / IntlChar / UConverter / Spoofchecker / IntlException - * all gate on {@see IntlExtensionPolicy} advertisement (no phantom class_exists; #6366, #6171, #6139, - * #6188, #20035, #20740, #20822). + * NumberFormatter / Normalizer / Collator / MessageFormatter / IntlListFormatter / + * Transliterator / ResourceBundle / IntlBreakIterator / IntlCodePointBreakIterator / + * IntlChar / UConverter / Spoofchecker / IntlException all gate on {@see IntlExtensionPolicy} + * advertisement (no phantom class_exists; #6366, #6171, #6139, #6188, #20035, #20740, #20822, #23229). */ final class BuiltinClasses { @@ -69,6 +69,9 @@ public static function register(Context $ctx): void VmNumberFormatter::registerClass($ctx); VmCollator::registerClass($ctx); VmMessageFormatter::registerClass($ctx); + if (IntlExtensionPolicy::advertisesIntlListFormatter()) { + VmIntlListFormatter::registerClass($ctx); + } VmTransliterator::registerClass($ctx); VmResourceBundle::registerClass($ctx); VmBreakIterator::registerClass($ctx); @@ -259,6 +262,17 @@ public static function registerMessageFormatter(Context $ctx): void } } + /** IntlListFormatter — PHP 8.5+ + host intl (#23229). */ + public static function registerIntlListFormatter(Context $ctx): void + { + $before = array_keys($ctx->classes); + VmIntlListFormatter::registerClass($ctx); + self::registerIntlException($ctx); + foreach (array_diff(array_keys($ctx->classes), $before) as $lc) { + $ctx->classes[$lc]->isInternal = true; + } + } + public static function registerTransliterator(Context $ctx): void { $before = array_keys($ctx->classes); diff --git a/ext/intl/IntlExtensionPolicy.php b/ext/intl/IntlExtensionPolicy.php index 301d5365be..23fd719811 100644 --- a/ext/intl/IntlExtensionPolicy.php +++ b/ext/intl/IntlExtensionPolicy.php @@ -211,6 +211,17 @@ public static function advertisesMessageFormatter(): bool return self::advertisesBuiltins(); } + /** + * IntlListFormatter — PHP 8.5+ + loaded ext/intl (php-src ext/intl/listformatter; #23229). + * + * Withheld when host intl is off or language profile < 8.5 (no phantom class_exists). + */ + public static function advertisesIntlListFormatter(): bool + { + return self::advertisesBuiltins() + && CompilerVersion::advertisesIntlListFormatter(); + } + /** * Transliterator / transliterator_* — require loaded ext/intl (php-src transliterator; #6139, #19670). */ diff --git a/ext/intl/IntlSerializeDeny.php b/ext/intl/IntlSerializeDeny.php index 3c88675822..080849aa38 100644 --- a/ext/intl/IntlSerializeDeny.php +++ b/ext/intl/IntlSerializeDeny.php @@ -12,6 +12,7 @@ * - NumberFormatter — ext/intl/formatter/formatter.stub.php (#23092) * - Collator — ext/intl/collator/collator.stub.php (#23092) * - MessageFormatter — ext/intl/msgformat/msgformat.stub.php (#23092) + * - IntlListFormatter — ext/intl/listformatter/listformatter.stub.php (#23229) * - ResourceBundle — ext/intl/resourcebundle/resourcebundle.stub.php (#23092) * - IntlCalendar / IntlGregorianCalendar (+ subclasses) — ext/intl/calendar/calendar.stub.php (#23092) * - Transliterator — ext/intl/transliterator/transliterator.stub.php (#23136) @@ -28,6 +29,7 @@ final class IntlSerializeDeny VmNumberFormatter::CLASS_LC, VmCollator::CLASS_LC, VmMessageFormatter::CLASS_LC, + VmIntlListFormatter::CLASS_LC, VmResourceBundle::CLASS_LC, VmIntlCalendar::CLASS_LC, VmIntlCalendar::GREGORIAN_CLASS_LC, diff --git a/ext/intl/Module.php b/ext/intl/Module.php index c249b2ca16..0778c2f1bf 100644 --- a/ext/intl/Module.php +++ b/ext/intl/Module.php @@ -60,6 +60,9 @@ public function init(Runtime $runtime): void if (IntlExtensionPolicy::advertisesMessageFormatter()) { BuiltinClasses::registerMessageFormatter($runtime->vmContext); } + if (IntlExtensionPolicy::advertisesIntlListFormatter()) { + BuiltinClasses::registerIntlListFormatter($runtime->vmContext); + } if (IntlExtensionPolicy::advertisesTransliterator()) { BuiltinClasses::registerTransliterator($runtime->vmContext); } diff --git a/ext/intl/VmIntlListFormatter.php b/ext/intl/VmIntlListFormatter.php new file mode 100644 index 0000000000..49697c24ca --- /dev/null +++ b/ext/intl/VmIntlListFormatter.php @@ -0,0 +1,656 @@ + + */ + private static array $state = []; + + private static ?\FFI $ffi = null; + + private static string $symSuffix = ''; + + private static bool $ffiUnavailable = false; + + public static function registerClass(Context $ctx): void + { + if (isset($ctx->classes[self::CLASS_LC])) { + return; + } + + $entry = new ClassEntry('IntlListFormatter'); + $entry->isInternal = true; + $entry->isFinal = true; + foreach (self::classConstants() as $name => $value) { + $lc = strtolower($name); + $const = new Variable(Variable::TYPE_INTEGER); + $const->int($value); + $entry->constants[$lc] = $const; + $entry->constNames[$lc] = $name; + } + $pub = CfgFunc::FLAG_PUBLIC; + $construct = new IntlListFormatterConstruct(); + $entry->constructor = $construct; + $methods = [ + '__construct' => [$construct, $pub, '__construct'], + 'format' => [new IntlListFormatterFormat(), $pub, 'format'], + 'geterrorcode' => [new IntlListFormatterGetErrorCode(), $pub, 'getErrorCode'], + 'geterrormessage' => [new IntlListFormatterGetErrorMessage(), $pub, 'getErrorMessage'], + ]; + foreach ($methods as $lc => [$handler, $vis, $name]) { + $entry->methods[$lc] = $handler; + $entry->methodVisibility[$lc] = $vis; + $entry->methodNames[$lc] = $name; + } + $ctx->classes[self::CLASS_LC] = $entry; + } + + /** + * @return array + */ + public static function classConstants(): array + { + $consts = [ + 'TYPE_AND' => self::TYPE_AND, + 'WIDTH_WIDE' => self::WIDTH_WIDE, + ]; + if (self::supportsTypedListFormatter()) { + $consts['TYPE_OR'] = self::TYPE_OR; + $consts['TYPE_UNITS'] = self::TYPE_UNITS; + $consts['WIDTH_SHORT'] = self::WIDTH_SHORT; + $consts['WIDTH_NARROW'] = self::WIDTH_NARROW; + } + + return $consts; + } + + /** ICU ≥67 typed ListFormatter (php-src stub `#if U_ICU_VERSION_MAJOR_NUM >= 67`). */ + public static function supportsTypedListFormatter(): bool + { + return IntlExtensionPolicy::icuMajorVersion() >= 67 + || (0 === IntlExtensionPolicy::icuMajorVersion() && null !== self::ffi()); + } + + public static function isListFormatterObject(?ObjectEntry $object): bool + { + return null !== $object && self::CLASS_LC === strtolower($object->class->name); + } + + /** + * Shared construct path — php-src IntlListFormatter::__construct. + * + * @throws \ValueError|\IntlException|\Error + */ + public static function initObject(ObjectEntry $object, string $locale, int $type, int $width): void + { + if ($object->constructed || isset(self::$state[$object->id])) { + throw new \Error('IntlListFormatter object is already constructed'); + } + if ('' === $locale) { + $locale = VmLocale::getDefault(); + } + if (\strlen($locale) > self::MAX_LOCALE_LEN) { + throw new \ValueError(\sprintf( + 'IntlListFormatter::__construct(): Argument #1 ($locale) must be less than or equal to %d characters', + self::MAX_LOCALE_LEN + )); + } + if (!self::isValidLocaleLanguage($locale)) { + throw new \ValueError(\sprintf( + 'IntlListFormatter::__construct(): Argument #1 ($locale) "%s" is invalid', + $locale + )); + } + if (self::supportsTypedListFormatter()) { + if (self::TYPE_AND !== $type && self::TYPE_OR !== $type && self::TYPE_UNITS !== $type) { + throw new \ValueError( + 'IntlListFormatter::__construct(): Argument #2 ($type) must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS' + ); + } + if (self::WIDTH_WIDE !== $width && self::WIDTH_SHORT !== $width && self::WIDTH_NARROW !== $width) { + throw new \ValueError( + 'IntlListFormatter::__construct(): Argument #3 ($width) must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW' + ); + } + } else { + if (self::TYPE_AND !== $type) { + throw new \ValueError( + 'IntlListFormatter::__construct(): Argument #2 ($type) contains an unsupported type. ICU 66 and below only support IntlListFormatter::TYPE_AND' + ); + } + if (self::WIDTH_WIDE !== $width) { + throw new \ValueError( + 'IntlListFormatter::__construct(): Argument #3 ($width) contains an unsupported width. ICU 66 and below only support IntlListFormatter::WIDTH_WIDE' + ); + } + } + + $handle = self::openFormatter($locale, $type, $width); + if (null === $handle) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'Constructor failed' + ); + throw new \IntlException('Constructor failed'); + } + + $object->constructed = true; + self::$state[$object->id] = [ + 'handle' => $handle, + 'locale' => $locale, + 'type' => $type, + 'width' => $width, + 'errorCode' => IntlError::U_ZERO_ERROR, + 'errorMessage' => 'U_ZERO_ERROR', + ]; + IntlError::clear(); + } + + /** + * @param list $strings + */ + public static function format(ObjectEntry $object, array $strings): string|false + { + if (!isset(self::$state[$object->id])) { + self::setObjectError( + $object, + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'Failed to format list: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + self::clearObjectError($object); + if ([] === $strings) { + return ''; + } + $handle = self::$state[$object->id]['handle']; + if (null === $handle) { + self::setObjectError( + $object, + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'Failed to format list: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + $result = self::formatWithHandle($handle, $strings); + if (null === $result) { + $code = IntlError::getCode(); + $msg = IntlError::getMessage(); + if (IntlError::U_ZERO_ERROR === $code) { + $code = IntlError::U_ILLEGAL_ARGUMENT_ERROR; + $msg = 'Failed to format list'; + } + self::setObjectError($object, $code, $msg); + + return false; + } + + return $result; + } + + public static function getErrorCode(ObjectEntry $object): int + { + return self::$state[$object->id]['errorCode'] ?? IntlError::U_ZERO_ERROR; + } + + public static function getErrorMessage(ObjectEntry $object): string + { + return self::$state[$object->id]['errorMessage'] ?? 'U_ZERO_ERROR'; + } + + public static function coerceLocaleArg(Variable $var, string $function, int $position): string + { + return VmString::coerceStringBuiltinArg($var, $function, $position, 'locale'); + } + + public static function coerceIntArg(Variable $var, string $function, int $position, string $name): int + { + $var = $var->resolveIndirect(); + if (Variable::TYPE_INTEGER === $var->type) { + return $var->toInt(); + } + if (Variable::TYPE_FLOAT === $var->type) { + return (int) $var->toFloat(); + } + if (Variable::TYPE_BOOLEAN === $var->type) { + return $var->toBool() ? 1 : 0; + } + if (Variable::TYPE_NULL === $var->type || Variable::TYPE_UNDEFINED === $var->type) { + return 0; + } + if (Variable::TYPE_STRING === $var->type && is_numeric($var->toString())) { + return (int) $var->toString(); + } + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($%s) must be of type int, %s given', + $function, + $position + 1, + $name, + ReflectionSupport::valueTypeLabelPublic($var) + )); + } + + public static function coerceStringList(Variable $var, Frame $frame, string $function, int $position): array + { + $var = $var->resolveIndirect(); + if (Variable::TYPE_ARRAY !== $var->type) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($strings) must be of type array, %s given', + $function, + $position + 1, + ReflectionSupport::valueTypeLabelPublic($var) + )); + } + /** @var HashTable $ht */ + $ht = $var->toArray(); + $vm = $frame->vmContext?->runtime?->vm; + $out = []; + foreach ($ht->iterate(true) as $item) { + if (null !== $vm) { + $out[] = $vm->coerceVariableToString($item, $frame); + } else { + $out[] = $item->toString(); + } + } + + return $out; + } + + private static function isValidLocaleLanguage(string $locale): bool + { + $ffi = self::ffi(); + if (null === $ffi) { + // Without ICU, accept BCP-47-ish tags so construct can still fail at open. + return 1 === preg_match('/^[A-Za-z]/', $locale); + } + $fn = 'uloc_getISO3Language'.self::$symSuffix; + try { + $iso3 = $ffi->$fn($locale); + + return \is_string($iso3) && '' !== $iso3; + } catch (\Throwable) { + return 1 === preg_match('/^[A-Za-z]/', $locale); + } + } + + /** @return object|null FFI UListFormatter* */ + private static function openFormatter(string $locale, int $type, int $width): ?object + { + $ffi = self::ffi(); + if (null === $ffi) { + return null; + } + $open = 'ulistfmt_openForType'.self::$symSuffix; + try { + $status = \FFI::new('int32_t'); + $status->cdata = 0; + $handle = $ffi->$open($locale, $type, $width, \FFI::addr($status)); + // U_FAILURE: status > 0 (warnings are negative). + if (null === $handle || $status->cdata > 0) { + return null; + } + + return $handle; + } catch (\Throwable) { + return null; + } + } + + /** + * @param list $strings + */ + private static function formatWithHandle(object $handle, array $strings): ?string + { + $ffi = self::ffi(); + if (null === $ffi) { + return null; + } + $fromUtf8 = 'u_strFromUTF8'.self::$symSuffix; + $toUtf8 = 'u_strToUTF8'.self::$symSuffix; + $format = 'ulistfmt_format'.self::$symSuffix; + $n = \count($strings); + try { + $ptrs = $ffi->new('uint16_t*['.$n.']'); + $lens = $ffi->new('int32_t['.$n.']'); + $keep = []; + for ($i = 0; $i < $n; ++$i) { + $u = self::utf8ToUChar($strings[$i]); + if (null === $u) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'Failed to convert string to UTF-16' + ); + + return null; + } + [$buf, $len] = $u; + $keep[] = $buf; + $ptrs[$i] = $ffi->cast('uint16_t*', $buf); + $lens[$i] = $len; + } + $status = \FFI::new('int32_t'); + $status->cdata = 0; + $needed = $ffi->$format($handle, $ptrs, $lens, $n, null, 0, \FFI::addr($status)); + // Expected U_BUFFER_OVERFLOW_ERROR (-124) when dest is null. + $status->cdata = 0; + $cap = max(1, (int) $needed + 1); + $res = $ffi->new('uint16_t['.$cap.']'); + $len = $ffi->$format($handle, $ptrs, $lens, $n, $res, $cap, \FFI::addr($status)); + if ($status->cdata > 0) { + IntlError::set((int) $status->cdata, 'Failed to format list'); + + return null; + } + $outLen = \FFI::new('int32_t'); + $outStatus = \FFI::new('int32_t'); + $outStatus->cdata = 0; + $ffi->$toUtf8(null, 0, \FFI::addr($outLen), $res, $len, \FFI::addr($outStatus)); + $outStatus->cdata = 0; + $outCap = max(1, (int) $outLen->cdata + 1); + $out = $ffi->new('char['.$outCap.']'); + $ffi->$toUtf8($out, $outCap, \FFI::addr($outLen), $res, $len, \FFI::addr($outStatus)); + if ($outStatus->cdata > 0) { + IntlError::set((int) $outStatus->cdata, 'Failed to convert result to UTF-8'); + + return null; + } + + return \FFI::string($out, (int) $outLen->cdata); + } catch (\Throwable) { + IntlError::set(IntlError::U_ILLEGAL_ARGUMENT_ERROR, 'Failed to format list'); + + return null; + } + } + + /** @return array{0: object, 1: int}|null */ + private static function utf8ToUChar(string $utf8): ?array + { + $ffi = self::ffi(); + if (null === $ffi) { + return null; + } + $fromUtf8 = 'u_strFromUTF8'.self::$symSuffix; + try { + $len = \FFI::new('int32_t'); + $status = \FFI::new('int32_t'); + $status->cdata = 0; + $ffi->$fromUtf8(null, 0, \FFI::addr($len), $utf8, \strlen($utf8), \FFI::addr($status)); + $status->cdata = 0; + $cap = max(1, $len->cdata + 1); + $buf = \FFI::new('uint16_t['.$cap.']'); + $ffi->$fromUtf8($buf, $cap, \FFI::addr($len), $utf8, \strlen($utf8), \FFI::addr($status)); + if ($status->cdata > 0) { + return null; + } + + return [$buf, (int) $len->cdata]; + } catch (\Throwable) { + return null; + } + } + + private static function setObjectError(ObjectEntry $object, int $code, string $message): void + { + if (isset(self::$state[$object->id])) { + self::$state[$object->id]['errorCode'] = $code; + self::$state[$object->id]['errorMessage'] = $message; + } + IntlError::set($code, $message); + } + + private static function clearObjectError(ObjectEntry $object): void + { + if (isset(self::$state[$object->id])) { + self::$state[$object->id]['errorCode'] = IntlError::U_ZERO_ERROR; + self::$state[$object->id]['errorMessage'] = 'U_ZERO_ERROR'; + } + IntlError::clear(); + } + + private static function ffi(): ?\FFI + { + if (self::$ffiUnavailable) { + return null; + } + if (null !== self::$ffi) { + return self::$ffi; + } + if (!\class_exists(\FFI::class, false) && !\extension_loaded('FFI')) { + self::$ffiUnavailable = true; + + return null; + } + $candidates = [ + ['libicui18n.so.74', '_74'], + ['libicui18n.so.72', '_72'], + ['libicui18n.so.71', '_71'], + ['libicui18n.so.70', '_70'], + ['/usr/lib/x86_64-linux-gnu/libicui18n.so.74', '_74'], + ['/usr/lib/x86_64-linux-gnu/libicui18n.so.70', '_70'], + ['libicui18n.so', '_70'], + ['libicui18n.dylib', ''], + ]; + foreach ($candidates as [$lib, $suffix]) { + try { + self::$ffi = \FFI::cdef(self::cdefForSuffix($suffix), $lib); + self::$symSuffix = $suffix; + + return self::$ffi; + } catch (\Throwable) { + self::$ffi = null; + } + } + self::$ffiUnavailable = true; + + return null; + } + + private static function cdefForSuffix(string $suffix): string + { + return <<calledArgs); + if ($argc < 2 || $argc > 4) { + throw new \ArgumentCountError(\sprintf( + 'IntlListFormatter::__construct() expects between 1 and 3 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlListFormatter::isListFormatterObject($receiver->toObject())) { + throw new \Error('IntlListFormatter::__construct() called on incompatible object'); + } + $locale = VmIntlListFormatter::coerceLocaleArg( + $frame->calledArgs[1], + 'IntlListFormatter::__construct', + 0 + ); + $type = VmIntlListFormatter::TYPE_AND; + $width = VmIntlListFormatter::WIDTH_WIDE; + if ($argc >= 3) { + $type = VmIntlListFormatter::coerceIntArg( + $frame->calledArgs[2], + 'IntlListFormatter::__construct', + 1, + 'type' + ); + } + if ($argc >= 4) { + $width = VmIntlListFormatter::coerceIntArg( + $frame->calledArgs[3], + 'IntlListFormatter::__construct', + 2, + 'width' + ); + } + VmIntlListFormatter::initObject($receiver->toObject(), $locale, $type, $width); + } +} + +/** IntlListFormatter::format() — php-src listformatter_class.cpp (#23229). */ +final class IntlListFormatterFormat extends VmClassMethod +{ + public function __construct() + { + parent::__construct('format'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (2 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlListFormatter::format() expects exactly 1 argument, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlListFormatter::isListFormatterObject($receiver->toObject())) { + throw new \Error('IntlListFormatter::format() called on incompatible object'); + } + $strings = VmIntlListFormatter::coerceStringList( + $frame->calledArgs[1], + $frame, + 'IntlListFormatter::format', + 0 + ); + if (null === $frame->returnVar) { + return; + } + $result = VmIntlListFormatter::format($receiver->toObject(), $strings); + if (false === $result) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->string($result); + } +} + +/** IntlListFormatter::getErrorCode() — php-src listformatter_class.cpp (#23229). */ +final class IntlListFormatterGetErrorCode extends VmClassMethod +{ + public function __construct() + { + parent::__construct('getErrorCode'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (1 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlListFormatter::getErrorCode() expects exactly 0 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlListFormatter::isListFormatterObject($receiver->toObject())) { + throw new \Error('IntlListFormatter::getErrorCode() called on incompatible object'); + } + if (null !== $frame->returnVar) { + $frame->returnVar->int(VmIntlListFormatter::getErrorCode($receiver->toObject())); + } + } +} + +/** IntlListFormatter::getErrorMessage() — php-src listformatter_class.cpp (#23229). */ +final class IntlListFormatterGetErrorMessage extends VmClassMethod +{ + public function __construct() + { + parent::__construct('getErrorMessage'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (1 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlListFormatter::getErrorMessage() expects exactly 0 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlListFormatter::isListFormatterObject($receiver->toObject())) { + throw new \Error('IntlListFormatter::getErrorMessage() called on incompatible object'); + } + if (null !== $frame->returnVar) { + $frame->returnVar->string(VmIntlListFormatter::getErrorMessage($receiver->toObject())); + } + } +} diff --git a/lib/CompilerVersion.php b/lib/CompilerVersion.php index 50b01db3a3..6fcd38e3b5 100644 --- a/lib/CompilerVersion.php +++ b/lib/CompilerVersion.php @@ -1153,6 +1153,17 @@ public static function advertisesLocaleDisplayKeyword(): bool return self::advertisesLocalePhp85Apis(); } + /** + * PHP 8.5+ IntlListFormatter — ICU list formatting (php-src ext/intl/listformatter; #23229). + * + * php-src: listformatter.stub.php / listformatter_class.cpp (GH-18519). + * Enable via stable 8.5.0+ or explicit {@code PHP_COMPILER_PROFILE=8.5} forward profile. + */ + public static function advertisesIntlListFormatter(): bool + { + return self::advertisesLocalePhp85Apis(); + } + /** * PHP 8.4+ asymmetric property visibility (private(set), protected(set), …). * diff --git a/test/compliance/JITTest.php b/test/compliance/JITTest.php index a6feee0c28..6acdde9de9 100755 --- a/test/compliance/JITTest.php +++ b/test/compliance/JITTest.php @@ -744,6 +744,7 @@ public static function providePHPTests(): \Generator || str_contains($name, 'numberformatter') || str_contains($name, 'intlcalendar') || str_contains($name, 'msgfmt_format') + || str_contains($name, 'intl_list_formatter') || str_contains($name, 'transliterator') || str_contains($name, 'resourcebundle') || str_contains($name, 'intl_skeleton') @@ -1596,6 +1597,10 @@ public static function providePHPTests(): \Generator if (str_contains($name, 'spoofchecker_set_allowed_chars')) { continue; } + // IntlListFormatter: VM green (#23229); JIT lowering deferred (VmClassMethod VM-only). + if (str_contains($name, 'intl_list_formatter')) { + continue; + } // Phar instance API: VM green (#20628); jit.php hits MathBaseConvertRuntime::constFloat // (hexdec/WeakRef bootstrap) same as curl_share_init_persistent. if (str_contains($name, 'phar_instance_api')) { diff --git a/test/compliance/VMTest.php b/test/compliance/VMTest.php index d1ba1b65bb..26f9e3c4d8 100755 --- a/test/compliance/VMTest.php +++ b/test/compliance/VMTest.php @@ -612,6 +612,7 @@ public static function providePHPTests(): \Generator || str_contains($name, 'numberformatter') || str_contains($name, 'intlcalendar') || str_contains($name, 'msgfmt_format') + || str_contains($name, 'intl_list_formatter') || str_contains($name, 'transliterator') || str_contains($name, 'resourcebundle') || str_contains($name, 'intl_skeleton') diff --git a/test/compliance/cases/intl/intl_list_formatter_85.phpt b/test/compliance/cases/intl/intl_list_formatter_85.phpt new file mode 100644 index 0000000000..d7890012d3 --- /dev/null +++ b/test/compliance/cases/intl/intl_list_formatter_85.phpt @@ -0,0 +1,51 @@ +--TEST-- +IntlListFormatter on PROFILE=8.5 + host intl (#23229) +--SKIPIF-- + +--ENV-- +PHP_COMPILER_PROFILE=8.5 +--FILE-- +isFinal() ? '1' : '0', "\n"; +echo 'TYPE_AND=', (int) IntlListFormatter::TYPE_AND, "\n"; +echo 'WIDTH_WIDE=', (int) IntlListFormatter::WIDTH_WIDE, "\n"; + +$f = new IntlListFormatter('en_US'); +echo 'en=', $f->format(['A', 'B', 'C']), "\n"; +echo 'empty=', var_export($f->format([]), true), "\n"; +echo 'nums=', $f->format([1, 2, 3]), "\n"; + +$fOr = new IntlListFormatter('en_US', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE); +echo 'or=', $fOr->format(['A', 'B', 'C']), "\n"; + +$fr = new IntlListFormatter('FR', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE); +echo 'fr=', $fr->format([1, 2, 3]), "\n"; + +echo 'err=', $f->getErrorCode(), "\n"; +echo 'errmsg=', $f->getErrorMessage(), "\n"; +?> +--EXPECT-- +class=1 +final=1 +TYPE_AND=0 +WIDTH_WIDE=0 +en=A, B, and C +empty='' +nums=1, 2, and 3 +or=A, B, or C +fr=1, 2 et 3 +err=0 +errmsg=U_ZERO_ERROR diff --git a/test/compliance/cases/intl/intl_list_formatter_profile82.phpt b/test/compliance/cases/intl/intl_list_formatter_profile82.phpt new file mode 100644 index 0000000000..b663d6dc35 --- /dev/null +++ b/test/compliance/cases/intl/intl_list_formatter_profile82.phpt @@ -0,0 +1,20 @@ +--TEST-- +IntlListFormatter withheld on PROFILE=8.2 (#23229) +--SKIPIF-- + +--ENV-- +PHP_COMPILER_PROFILE=8.2 +--FILE-- + +--EXPECT-- +class=0 diff --git a/test/compliance/cases/stdlib/intl_phantom_classes.phpt b/test/compliance/cases/stdlib/intl_phantom_classes.phpt index 79cea2aaed..0643fa20ea 100644 --- a/test/compliance/cases/stdlib/intl_phantom_classes.phpt +++ b/test/compliance/cases/stdlib/intl_phantom_classes.phpt @@ -19,6 +19,7 @@ echo 'intliterator=', (int) class_exists('IntlIterator', false), "\n"; echo 'intlchar=', (int) class_exists('IntlChar', false), "\n"; echo 'uconverter=', (int) class_exists('UConverter', false), "\n"; echo 'spoofchecker=', (int) class_exists('Spoofchecker', false), "\n"; +echo 'intllistformatter=', (int) class_exists('IntlListFormatter', false), "\n"; echo 'locale_get_default=', (int) function_exists('locale_get_default'), "\n"; try { Collator::create('en_US'); @@ -57,6 +58,7 @@ intliterator=0 intlchar=0 uconverter=0 spoofchecker=0 +intllistformatter=0 locale_get_default=0 Error: Class "Collator" not found msgfmt_err=Error diff --git a/test/repro/maintainer_gap_intl_list_formatter_85.php b/test/repro/maintainer_gap_intl_list_formatter_85.php new file mode 100644 index 0000000000..e08b553a19 --- /dev/null +++ b/test/repro/maintainer_gap_intl_list_formatter_85.php @@ -0,0 +1,31 @@ +format(['A', 'B', 'C']); +echo 'format=', $out, "\n"; +if ('A, B, and C' !== $out) { + fwrite(STDERR, "FAIL: expected 'A, B, and C', got ".var_export($out, true)."\n"); + exit(1); +} + +$f2 = new IntlListFormatter('en_US', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE); +echo 'format2=', $f2->format(['A', 'B', 'C']), "\n"; + +echo "ok\n"; diff --git a/test/repro/maintainer_gap_intl_list_formatter_absent.php b/test/repro/maintainer_gap_intl_list_formatter_absent.php new file mode 100644 index 0000000000..fd0e1e8c10 --- /dev/null +++ b/test/repro/maintainer_gap_intl_list_formatter_absent.php @@ -0,0 +1,19 @@ +prevProfile = false === $prev ? null : $prev; + } + + protected function tearDown(): void + { + if (null === $this->prevProfile) { + putenv('PHP_COMPILER_PROFILE'); + } else { + putenv('PHP_COMPILER_PROFILE='.$this->prevProfile); + } + } + + public function testAdvertisedOnProfile85WithHostIntl(): void + { + if (!\extension_loaded('intl')) { + $this->markTestSkipped('host php-intl required'); + } + putenv('PHP_COMPILER_PROFILE=8.5'); + $this->assertTrue(CompilerVersion::advertisesIntlListFormatter()); + $this->assertTrue(IntlExtensionPolicy::advertisesIntlListFormatter()); + + $runtime = new Runtime(); + $this->assertArrayHasKey(VmIntlListFormatter::CLASS_LC, $runtime->vmContext->classes); + + $object = new ObjectEntry($runtime->vmContext->classes[VmIntlListFormatter::CLASS_LC]); + VmIntlListFormatter::initObject($object, 'en_US', VmIntlListFormatter::TYPE_AND, VmIntlListFormatter::WIDTH_WIDE); + $this->assertSame('A, B, and C', VmIntlListFormatter::format($object, ['A', 'B', 'C'])); + } + + public function testWithheldOnProfile82(): void + { + if (!\extension_loaded('intl')) { + $this->markTestSkipped('host php-intl required'); + } + putenv('PHP_COMPILER_PROFILE=8.2'); + $this->assertFalse(CompilerVersion::advertisesIntlListFormatter()); + $this->assertFalse(IntlExtensionPolicy::advertisesIntlListFormatter()); + + $runtime = new Runtime(); + $this->assertArrayNotHasKey(VmIntlListFormatter::CLASS_LC, $runtime->vmContext->classes); + } + + public function testSerializeDenied(): void + { + putenv('PHP_COMPILER_PROFILE=8.5'); + $runtime = new Runtime(); + VmIntlListFormatter::registerClass($runtime->vmContext); + $object = new ObjectEntry($runtime->vmContext->classes[VmIntlListFormatter::CLASS_LC]); + $var = new Variable(Variable::TYPE_OBJECT); + $var->object($object); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Serialization of 'IntlListFormatter' is not allowed"); + VmSerialize::serializeValue($runtime->vmContext, $var); + } +}