diff --git a/UPGRADING b/UPGRADING index 5a5cafc0234f..18fe84ecfdc0 100644 --- a/UPGRADING +++ b/UPGRADING @@ -234,6 +234,13 @@ PHP 8.6 UPGRADE NOTES and DirectoryIterator::current() returns string|SplFileInfo|static. - Standard: + . During sort() and rsort() on packed arrays, implicit comparison callbacks + (such as __toString() and error handlers) can now read numeric indices of + the array and observe its partially sorted contents. Previously, these + reads reported an undefined array key warning and returned null because + the hash index was cleared during sorting. This applies to all array sizes + and comparison flags that invoke such callbacks. Arrays stored in mixed + form continue to use the previous sorting path. . array_intersect() with at least two arrays now converts values to strings while scanning its inputs instead of during sort comparisons. This can change the number and order of conversion warnings and __toString() calls, @@ -983,6 +990,8 @@ PHP 8.6 UPGRADE NOTES . Improved performance of array_walk(). . Improved performance of intval('+0b...', 2) and intval('0b...', 2). . Improved performance of str_split(). + . sort(), rsort(), and usort() now avoid converting packed arrays to mixed + storage while sorting. usort() continues to sort a copy of the input array. - URI: . Improved performance of Uri\WhatWg\Url::parse() when collecting diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index bd671018e038..b63313e5cbfc 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -187,6 +187,19 @@ PHP 8.6 INTERNALS UPGRADE NOTES . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were added, given the primary flags were running out of bits. . Added zend_hash_str_lookup(). + . Added zend_hash_sort_packed() to sort and renumber packed arrays in place + without converting them to mixed storage. The array must have a reference + count of 1. Holes are compacted and iterator positions are relocated, + including cursors at holes and one past the end. The comparator receives + zval pointers and must use the original positions initialized in + Z_EXTRA_P() to break ties stably. It must not execute user code or modify + the array. Empty arrays retain their internal pointer and next free element. + . Added zend_array_sort_packed() to sort and renumber packed user arrays + without converting them to mixed storage. The array must have a reference + count of 1. Holes are compacted, and the array is kept alive while the + comparator runs, including calls to user code. The comparator receives + zval pointers and must use the original positions initialized in + Z_EXTRA_P() to break ties stably. . Added zend_ast_call_get_args() to fetch the argument node from any call node. . Added Z_PARAM_ENUM(). diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 99406f9e4192..fec449e43e22 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -2990,6 +2990,90 @@ ZEND_API void zend_hash_bucket_packed_swap(Bucket *p, Bucket *q) q->h = h; } +static void zend_hash_packed_zval_swap(void *a, void *b) +{ + zval tmp = *(zval *) a; + *(zval *) a = *(zval *) b; + *(zval *) b = tmp; +} + +static void zend_hash_sort_packed_prepare(HashTable *ht) +{ + uint32_t old_num_used = ht->nNumUsed; + uint32_t iter_pos = HT_INVALID_IDX; + uint32_t count = 0; + for (uint32_t i = 0; i < ht->nNumUsed; i++) { + zval *value = &ht->arPacked[i]; + if (UNEXPECTED(Z_TYPE_P(value) == IS_UNDEF)) { + if (count == i && UNEXPECTED(HT_HAS_ITERATORS(ht))) { + /* As in rehash, cursors at the first hole already have the right + * destination. Relocate subsequent cursors as values move. */ + iter_pos = zend_hash_iterators_lower_pos(ht, i + 1); + } + continue; + } + if (count != i) { + ht->arPacked[count] = *value; + /* Comparators may observe current() before sorting resets the pointer. */ + if (UNEXPECTED(ht->nInternalPointer > count && ht->nInternalPointer <= i)) { + ht->nInternalPointer = count; + } + if (UNEXPECTED(i >= iter_pos)) { + do { + zend_hash_iterators_update(ht, iter_pos, count); + iter_pos = zend_hash_iterators_lower_pos(ht, iter_pos + 1); + } while (iter_pos <= i); + } + } + Z_EXTRA(ht->arPacked[count]) = count; + count++; + } + ht->nNumUsed = count; + if (count != old_num_used && UNEXPECTED(HT_HAS_ITERATORS(ht))) { + /* A cursor at the old end must still see elements appended after sorting. */ + _zend_hash_iterators_update(ht, old_num_used, count); + } +} + +static void zend_hash_sort_packed_internal(HashTable *ht, compare_func_t compar) +{ + IS_CONSISTENT(ht); + ZEND_ASSERT(HT_IS_PACKED(ht)); + if (ht->nNumOfElements == 0) { + return; + } + + /* Compact holes and record the original order for stable comparisons. */ + zend_hash_sort_packed_prepare(ht); + zend_sort(ht->arPacked, ht->nNumUsed, sizeof(zval), compar, zend_hash_packed_zval_swap); + ht->nInternalPointer = 0; + ht->nNextFreeElement = ht->nNumUsed; +} + +ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compar) +{ + HT_ASSERT_RC1(ht); + zend_hash_sort_packed_internal(ht, compar); +} + +static zend_always_inline void zend_array_sort_release(HashTable *ht) +{ + if (UNEXPECTED(GC_DELREF(ht) == 0)) { + zend_array_destroy(ht); + } else { + gc_check_possible_root((zend_refcounted *) ht); + } +} + +ZEND_API void ZEND_FASTCALL zend_array_sort_packed(HashTable *ht, compare_func_t compar) +{ + HT_ASSERT_RC1(ht); + /* Keep the buffer alive and force PHP writes during comparison to separate. */ + GC_ADDREF(ht); + zend_hash_sort_packed_internal(ht, compar); + zend_array_sort_release(ht); +} + static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber) { Bucket *p; @@ -3102,11 +3186,7 @@ ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort, zend_hash_sort_internal(ht, sort, compar, renumber); - if (UNEXPECTED(GC_DELREF(ht) == 0)) { - zend_array_destroy(ht); - } else { - gc_check_possible_root((zend_refcounted *)ht); - } + zend_array_sort_release(ht); } static zend_always_inline int zend_hash_compare_impl(const HashTable *ht1, const HashTable *ht2, compare_func_t compar, bool ordered) { diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index 1181bee29fae..a86e781f9602 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -304,6 +304,19 @@ ZEND_API int zend_hash_compare(HashTable *ht1, const HashTable *ht2, compare_f ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); +/* Sort and renumber an exclusively owned packed array, compacting holes. The + * comparator receives zvals and must use the original positions initialized + * in Z_EXTRA_P() to break ties stably. It must not execute user code or modify + * the array. Compaction relocates iterators to their new positions, including + * cursors at holes and one past the end. + * Empty arrays retain their internal pointer and next free element. */ +ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compare_func); + +/* Sort and renumber a packed user array, compacting holes and keeping it alive + * across calls to user code. The comparator receives zvals and must use the + * original positions initialized in Z_EXTRA_P() to break ties stably. */ +ZEND_API void ZEND_FASTCALL zend_array_sort_packed(HashTable *ht, compare_func_t compare_func); + static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) { zend_hash_sort_ex(ht, zend_sort, compare_func, renumber); } diff --git a/ext/standard/array.c b/ext/standard/array.c index acf65c07bd67..5d8370f1d5c4 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -282,17 +282,17 @@ static zend_always_inline int php_array_key_compare_string_locale_unstable_i(Buc } /* }}} */ -static zend_always_inline int php_array_data_compare_unstable_i(Bucket *f, Bucket *s) /* {{{ */ +static zend_always_inline int php_array_data_compare_zval_unstable_i(zval *f, zval *s) /* {{{ */ { - int result = zend_compare(&f->val, &s->val); + int result = zend_compare(f, s); /* Special enums handling for array_unique. We don't want to add this logic to zend_compare as * that would be observable via comparison operators. */ - zval *rhs = &s->val; + zval *rhs = s; ZVAL_DEREF(rhs); if (UNEXPECTED(Z_TYPE_P(rhs) == IS_OBJECT) && result == ZEND_UNCOMPARABLE && (Z_OBJCE_P(rhs)->ce_flags & ZEND_ACC_ENUM)) { - zval *lhs = &f->val; + zval *lhs = f; ZVAL_DEREF(lhs); if (Z_TYPE_P(lhs) == IS_OBJECT && (Z_OBJCE_P(lhs)->ce_flags & ZEND_ACC_ENUM)) { // Order doesn't matter, we just need to group the same enum values @@ -308,29 +308,29 @@ static zend_always_inline int php_array_data_compare_unstable_i(Bucket *f, Bucke } /* }}} */ -static zend_always_inline int php_array_data_compare_numeric_unstable_i(Bucket *f, Bucket *s) /* {{{ */ +static zend_always_inline int php_array_data_compare_numeric_zval_unstable_i(zval *f, zval *s) /* {{{ */ { - return numeric_compare_function(&f->val, &s->val); + return numeric_compare_function(f, s); } /* }}} */ -static zend_always_inline int php_array_data_compare_string_case_unstable_i(Bucket *f, Bucket *s) /* {{{ */ +static zend_always_inline int php_array_data_compare_string_case_zval_unstable_i(zval *f, zval *s) /* {{{ */ { - return string_case_compare_function(&f->val, &s->val); + return string_case_compare_function(f, s); } /* }}} */ -static zend_always_inline int php_array_data_compare_string_unstable_i(Bucket *f, Bucket *s) /* {{{ */ +static zend_always_inline int php_array_data_compare_string_zval_unstable_i(zval *f, zval *s) /* {{{ */ { - return string_compare_function(&f->val, &s->val); + return string_compare_function(f, s); } /* }}} */ -static int php_array_natural_general_compare(Bucket *f, Bucket *s, bool fold_case) /* {{{ */ +static int php_array_natural_general_compare(zval *f, zval *s, bool fold_case) /* {{{ */ { zend_string *tmp_str1, *tmp_str2; - zend_string *str1 = zval_get_tmp_string(&f->val, &tmp_str1); - zend_string *str2 = zval_get_tmp_string(&s->val, &tmp_str2); + zend_string *str1 = zval_get_tmp_string(f, &tmp_str1); + zend_string *str2 = zval_get_tmp_string(s, &tmp_str2); int result = strnatcmp_ex(ZSTR_VAL(str1), ZSTR_LEN(str1), ZSTR_VAL(str2), ZSTR_LEN(str2), fold_case); @@ -340,36 +340,62 @@ static int php_array_natural_general_compare(Bucket *f, Bucket *s, bool fold_cas } /* }}} */ -static zend_always_inline int php_array_natural_compare_unstable_i(Bucket *a, Bucket *b) /* {{{ */ +static zend_always_inline int php_array_natural_compare_zval_unstable_i(zval *a, zval *b) /* {{{ */ { return php_array_natural_general_compare(a, b, false); } /* }}} */ -static zend_always_inline int php_array_natural_case_compare_unstable_i(Bucket *a, Bucket *b) /* {{{ */ +static zend_always_inline int php_array_natural_case_compare_zval_unstable_i(zval *a, zval *b) /* {{{ */ { return php_array_natural_general_compare(a, b, true); } /* }}} */ -static int php_array_data_compare_string_locale_unstable_i(Bucket *f, Bucket *s) /* {{{ */ +static int php_array_data_compare_string_locale_zval_unstable_i(zval *f, zval *s) /* {{{ */ { - return string_locale_compare_function(&f->val, &s->val); + return string_locale_compare_function(f, s); } /* }}} */ +static zend_never_inline ZEND_COLD int stable_zval_sort_fallback(const zval *a, const zval *b) +{ + return ZEND_THREEWAY_COMPARE(Z_EXTRA_P(a), Z_EXTRA_P(b)); +} + +/* Share value comparisons between Bucket sorting and packed zval sorting. */ +#define DEFINE_DATA_SORT_VARIANTS(name) \ + static zend_always_inline int php_array_##name##_unstable_i(Bucket *a, Bucket *b) { \ + return php_array_##name##_zval_unstable_i(&a->val, &b->val); \ + } \ + DEFINE_SORT_VARIANTS(name) \ + static zend_never_inline int php_array_packed_##name(const void *a, const void *b) { \ + int result = php_array_##name##_zval_unstable_i((zval *) a, (zval *) b); \ + if (EXPECTED(result)) { \ + return result; \ + } \ + return stable_zval_sort_fallback(a, b); \ + } \ + static zend_never_inline int php_array_packed_reverse_##name(const void *a, const void *b) { \ + int result = php_array_##name##_zval_unstable_i((zval *) a, (zval *) b) * -1; \ + if (EXPECTED(result)) { \ + return result; \ + } \ + return stable_zval_sort_fallback(a, b); \ + } + DEFINE_SORT_VARIANTS(key_compare); DEFINE_SORT_VARIANTS(key_compare_numeric); DEFINE_SORT_VARIANTS(key_compare_string_case); DEFINE_SORT_VARIANTS(key_compare_string); DEFINE_SORT_VARIANTS(key_compare_string_locale); -DEFINE_SORT_VARIANTS(data_compare); -DEFINE_SORT_VARIANTS(data_compare_numeric); -DEFINE_SORT_VARIANTS(data_compare_string_case); -DEFINE_SORT_VARIANTS(data_compare_string); -DEFINE_SORT_VARIANTS(data_compare_string_locale); -DEFINE_SORT_VARIANTS(natural_compare); -DEFINE_SORT_VARIANTS(natural_case_compare); +DEFINE_DATA_SORT_VARIANTS(data_compare); +DEFINE_DATA_SORT_VARIANTS(data_compare_numeric); +DEFINE_DATA_SORT_VARIANTS(data_compare_string_case); +DEFINE_DATA_SORT_VARIANTS(data_compare_string); +DEFINE_DATA_SORT_VARIANTS(data_compare_string_locale); +DEFINE_DATA_SORT_VARIANTS(natural_compare); +DEFINE_DATA_SORT_VARIANTS(natural_case_compare); static bucket_compare_func_t php_get_key_compare_func(zend_long sort_type) { @@ -431,129 +457,79 @@ static bucket_compare_func_t php_get_key_reverse_compare_func(zend_long sort_typ return NULL; } -static bucket_compare_func_t php_get_data_compare_func(zend_long sort_type) /* {{{ */ +typedef enum { + PHP_ARRAY_CMP_REGULAR, + PHP_ARRAY_CMP_NUMERIC, + PHP_ARRAY_CMP_STRING, + PHP_ARRAY_CMP_STRING_CASE, + PHP_ARRAY_CMP_NATURAL, + PHP_ARRAY_CMP_NATURAL_CASE, + PHP_ARRAY_CMP_LOCALE, +} php_array_compare_type; + +static zend_always_inline php_array_compare_type php_array_data_compare_type(zend_long sort_type) { switch (sort_type & ~PHP_SORT_FLAG_CASE) { case PHP_SORT_NUMERIC: - return php_array_data_compare_numeric; - + return PHP_ARRAY_CMP_NUMERIC; case PHP_SORT_STRING: - if (sort_type & PHP_SORT_FLAG_CASE) { - return php_array_data_compare_string_case; - } else { - return php_array_data_compare_string; - } - + return sort_type & PHP_SORT_FLAG_CASE ? PHP_ARRAY_CMP_STRING_CASE : PHP_ARRAY_CMP_STRING; case PHP_SORT_NATURAL: - if (sort_type & PHP_SORT_FLAG_CASE) { - return php_array_natural_case_compare; - } else { - return php_array_natural_compare; - } - + return sort_type & PHP_SORT_FLAG_CASE ? PHP_ARRAY_CMP_NATURAL_CASE : PHP_ARRAY_CMP_NATURAL; case PHP_SORT_LOCALE_STRING: - return php_array_data_compare_string_locale; - + return PHP_ARRAY_CMP_LOCALE; case PHP_SORT_REGULAR: default: - return php_array_data_compare; + return PHP_ARRAY_CMP_REGULAR; } - return NULL; } -static bucket_compare_func_t php_get_data_reverse_compare_func(zend_long sort_type) /* {{{ */ -{ - switch (sort_type & ~PHP_SORT_FLAG_CASE) { - case PHP_SORT_NUMERIC: - return php_array_reverse_data_compare_numeric; - - case PHP_SORT_STRING: - if (sort_type & PHP_SORT_FLAG_CASE) { - return php_array_reverse_data_compare_string_case; - } else { - return php_array_reverse_data_compare_string; - } - - case PHP_SORT_NATURAL: - if (sort_type & PHP_SORT_FLAG_CASE) { - return php_array_reverse_natural_case_compare; - } else { - return php_array_reverse_natural_compare; - } +#define PHP_ARRAY_DATA_COMPARATORS(name) { \ + {php_array_##name, php_array_reverse_##name}, \ + {php_array_##name##_unstable, php_array_reverse_##name##_unstable}, \ + {php_array_packed_##name, php_array_packed_reverse_##name} \ +} - case PHP_SORT_LOCALE_STRING: - return php_array_reverse_data_compare_string_locale; +/* Indexed by comparison type, then by reverse order. */ +static const struct { + bucket_compare_func_t stable[2]; + bucket_compare_func_t unstable[2]; + compare_func_t packed[2]; +} php_array_data_comparators[] = { + PHP_ARRAY_DATA_COMPARATORS(data_compare), + PHP_ARRAY_DATA_COMPARATORS(data_compare_numeric), + PHP_ARRAY_DATA_COMPARATORS(data_compare_string), + PHP_ARRAY_DATA_COMPARATORS(data_compare_string_case), + PHP_ARRAY_DATA_COMPARATORS(natural_compare), + PHP_ARRAY_DATA_COMPARATORS(natural_case_compare), + PHP_ARRAY_DATA_COMPARATORS(data_compare_string_locale), +}; +#undef PHP_ARRAY_DATA_COMPARATORS - case PHP_SORT_REGULAR: - default: - return php_array_reverse_data_compare; - } - return NULL; +static bucket_compare_func_t php_get_data_compare_func(zend_long sort_type) +{ + return php_array_data_comparators[php_array_data_compare_type(sort_type)].stable[false]; } -static bucket_compare_func_t php_get_data_compare_func_unstable(zend_long sort_type, bool reverse) /* {{{ */ +static bucket_compare_func_t php_get_data_reverse_compare_func(zend_long sort_type) { - switch (sort_type & ~PHP_SORT_FLAG_CASE) { - case PHP_SORT_NUMERIC: - if (reverse) { - return php_array_reverse_data_compare_numeric_unstable; - } else { - return php_array_data_compare_numeric_unstable; - } - break; - - case PHP_SORT_STRING: - if (sort_type & PHP_SORT_FLAG_CASE) { - if (reverse) { - return php_array_reverse_data_compare_string_case_unstable; - } else { - return php_array_data_compare_string_case_unstable; - } - } else { - if (reverse) { - return php_array_reverse_data_compare_string_unstable; - } else { - return php_array_data_compare_string_unstable; - } - } - break; + return php_array_data_comparators[php_array_data_compare_type(sort_type)].stable[true]; +} - case PHP_SORT_NATURAL: - if (sort_type & PHP_SORT_FLAG_CASE) { - if (reverse) { - return php_array_reverse_natural_case_compare_unstable; - } else { - return php_array_natural_case_compare_unstable; - } - } else { - if (reverse) { - return php_array_reverse_natural_compare_unstable; - } else { - return php_array_natural_compare_unstable; - } - } - break; +static compare_func_t php_get_packed_data_compare_func(zend_long sort_type) +{ + return php_array_data_comparators[php_array_data_compare_type(sort_type)].packed[false]; +} - case PHP_SORT_LOCALE_STRING: - if (reverse) { - return php_array_reverse_data_compare_string_locale_unstable; - } else { - return php_array_data_compare_string_locale_unstable; - } - break; +static compare_func_t php_get_packed_data_reverse_compare_func(zend_long sort_type) +{ + return php_array_data_comparators[php_array_data_compare_type(sort_type)].packed[true]; +} - case PHP_SORT_REGULAR: - default: - if (reverse) { - return php_array_reverse_data_compare_unstable; - } else { - return php_array_data_compare_unstable; - } - break; - } - return NULL; +static bucket_compare_func_t php_get_data_compare_func_unstable(zend_long sort_type, bool reverse) +{ + return php_array_data_comparators[php_array_data_compare_type(sort_type)].unstable[reverse]; } -/* }}} */ PHPAPI zend_long php_count_recursive(HashTable *ht) /* {{{ */ { @@ -694,8 +670,84 @@ PHP_FUNCTION(natcasesort) /* }}} */ typedef bucket_compare_func_t(*get_compare_function)(zend_long); +typedef compare_func_t (*get_packed_compare_function)(zend_long); + +static int php_array_packed_long_compare(const void *a, const void *b) +{ + const zval *lhs = a, *rhs = b; + return ZEND_THREEWAY_COMPARE(Z_LVAL_P(lhs), Z_LVAL_P(rhs)); +} + +static int php_array_packed_long_reverse_compare(const void *a, const void *b) +{ + const zval *lhs = a, *rhs = b; + return ZEND_THREEWAY_COMPARE(Z_LVAL_P(rhs), Z_LVAL_P(lhs)); +} + +static void php_array_packed_long_swap(void *a, void *b) +{ + zend_long tmp = Z_LVAL_P((zval *) a); + Z_LVAL_P((zval *) a) = Z_LVAL_P((zval *) b); + Z_LVAL_P((zval *) b) = tmp; +} + +static bool php_array_try_packed_scalar_sort(HashTable *array, compare_func_t cmp, + compare_func_t long_cmp) +{ + ZEND_ASSERT(GC_REFCOUNT(array) == 1); + ZEND_ASSERT(HT_IS_PACKED(array)); + uint32_t i = 0; + + if (long_cmp && HT_IS_WITHOUT_HOLES(array)) { + for (; i < array->nNumUsed; i++) { + if (Z_TYPE(array->arPacked[i]) != IS_LONG) { + break; + } + } + if (i == array->nNumUsed) { + if (array->nNumOfElements != 0) { + /* Direct integer ties are indistinguishable. Only the integer + * payload needs to move; no stability metadata is required. */ + zend_sort(array->arPacked, array->nNumUsed, sizeof(zval), long_cmp, + php_array_packed_long_swap); + array->nInternalPointer = 0; + array->nNextFreeElement = array->nNumUsed; + } + return true; + } + } + + /* Continue after any integer prefix. These direct values cannot invoke user + * code through a built-in comparator, so neither a lifetime pin nor possible + * GC root registration is needed. NAN is excluded because string coercion + * emits a warning, whose error handler can modify the array being sorted. */ + for (; i < array->nNumUsed; i++) { + zval *value = &array->arPacked[i]; + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_STRING: + break; + case IS_DOUBLE: + if (!zend_isnan(Z_DVAL_P(value))) { + break; + } + ZEND_FALLTHROUGH; + default: + return false; + } + } + + zend_hash_sort_packed(array, cmp); + return true; +} -static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compare_function get_cmp, bool renumber) { +static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, + get_compare_function get_cmp, bool renumber, get_packed_compare_function get_packed_cmp, + compare_func_t long_cmp) { HashTable *array; zend_long sort_type = PHP_SORT_REGULAR; bucket_compare_func_t cmp; @@ -706,8 +758,18 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compar Z_PARAM_LONG(sort_type) ZEND_PARSE_PARAMETERS_END(); - cmp = get_cmp(sort_type); + if (renumber && get_packed_cmp && HT_IS_PACKED(array)) { + compare_func_t packed_cmp = get_packed_cmp(sort_type); + if (php_array_data_compare_type(sort_type) != PHP_ARRAY_CMP_REGULAR) { + long_cmp = NULL; + } + if (!php_array_try_packed_scalar_sort(array, packed_cmp, long_cmp)) { + zend_array_sort_packed(array, packed_cmp); + } + RETURN_TRUE; + } + cmp = get_cmp(sort_type); zend_array_sort(array, cmp, renumber); RETURN_TRUE; @@ -716,52 +778,54 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compar /* {{{ Sort an array and maintain index association */ PHP_FUNCTION(asort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, false, NULL, NULL); } /* }}} */ /* {{{ Sort an array in reverse order and maintain index association */ PHP_FUNCTION(arsort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, false, NULL, NULL); } /* }}} */ /* {{{ Sort an array */ PHP_FUNCTION(sort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, true); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, true, + php_get_packed_data_compare_func, php_array_packed_long_compare); } /* }}} */ /* {{{ Sort an array in reverse order */ PHP_FUNCTION(rsort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, true); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, true, + php_get_packed_data_reverse_compare_func, php_array_packed_long_reverse_compare); } /* }}} */ /* {{{ Sort an array by key value in reverse order */ PHP_FUNCTION(krsort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_reverse_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_reverse_compare_func, false, NULL, NULL); } /* }}} */ /* {{{ Sort an array by key */ PHP_FUNCTION(ksort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_compare_func, false, NULL, NULL); } /* }}} */ -static inline int php_array_user_compare_unstable(Bucket *f, Bucket *s) /* {{{ */ +static inline int php_array_user_compare_zval_unstable(zval *f, zval *s) /* {{{ */ { zval args[2]; zval retval; - ZVAL_COPY_VALUE(&args[0], &f->val); - ZVAL_COPY_VALUE(&args[1], &s->val); + ZVAL_COPY_VALUE(&args[0], f); + ZVAL_COPY_VALUE(&args[1], s); BG(user_compare_fci).param_count = 2; BG(user_compare_fci).params = args; @@ -778,8 +842,8 @@ static inline int php_array_user_compare_unstable(Bucket *f, Bucket *s) /* {{{ * if (Z_TYPE(retval) == IS_FALSE) { /* Retry with swapped operands. */ - ZVAL_COPY_VALUE(&args[0], &s->val); - ZVAL_COPY_VALUE(&args[1], &f->val); + ZVAL_COPY_VALUE(&args[0], s); + ZVAL_COPY_VALUE(&args[1], f); zend_call_function(&BG(user_compare_fci), &BG(user_compare_fci_cache)); zend_long ret = php_get_long(&retval); @@ -792,12 +856,23 @@ static inline int php_array_user_compare_unstable(Bucket *f, Bucket *s) /* {{{ * } /* }}} */ +static inline int php_array_user_compare_unstable(Bucket *a, Bucket *b) +{ + return php_array_user_compare_zval_unstable(&a->val, &b->val); +} + static int php_array_user_compare(Bucket *a, Bucket *b) /* {{{ */ { RETURN_STABLE_SORT(a, b, php_array_user_compare_unstable(a, b)); } /* }}} */ +static int php_array_packed_user_compare(const void *a, const void *b) +{ + int result = php_array_user_compare_zval_unstable((zval *) a, (zval *) b); + return EXPECTED(result) ? result : stable_zval_sort_fallback(a, b); +} + #define PHP_ARRAY_CMP_FUNC_VARS \ zend_fcall_info old_user_compare_fci; \ zend_fcall_info_cache old_user_compare_fci_cache \ @@ -812,7 +887,8 @@ static int php_array_user_compare(Bucket *a, Bucket *b) /* {{{ */ BG(user_compare_fci) = old_user_compare_fci; \ BG(user_compare_fci_cache) = old_user_compare_fci_cache; \ -static void php_usort(INTERNAL_FUNCTION_PARAMETERS, bucket_compare_func_t compare_func, bool renumber) /* {{{ */ +static void php_usort(INTERNAL_FUNCTION_PARAMETERS, bucket_compare_func_t compare_func, + bool renumber, compare_func_t packed_compare_func) /* {{{ */ { zval *array; zend_array *arr; @@ -834,7 +910,11 @@ static void php_usort(INTERNAL_FUNCTION_PARAMETERS, bucket_compare_func_t compar /* Copy array, so the in-place modifications will not be visible to the callback function */ arr = zend_array_dup(arr); - zend_array_sort(arr, compare_func, renumber); + if (renumber && packed_compare_func && HT_IS_PACKED(arr)) { + zend_array_sort_packed(arr, packed_compare_func); + } else { + zend_array_sort(arr, compare_func, renumber); + } zval garbage; ZVAL_COPY_VALUE(&garbage, array); @@ -849,14 +929,15 @@ static void php_usort(INTERNAL_FUNCTION_PARAMETERS, bucket_compare_func_t compar /* {{{ Sort an array by values using a user-defined comparison function */ PHP_FUNCTION(usort) { - php_usort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_array_user_compare, true); + php_usort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_array_user_compare, true, + php_array_packed_user_compare); } /* }}} */ /* {{{ Sort an array with a user-defined comparison function and maintain index association */ PHP_FUNCTION(uasort) { - php_usort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_array_user_compare, false); + php_usort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_array_user_compare, false, NULL); } /* }}} */ @@ -923,7 +1004,7 @@ static int php_array_user_key_compare(Bucket *a, Bucket *b) /* {{{ */ /* {{{ Sort an array by keys using a user-defined comparison function */ PHP_FUNCTION(uksort) { - php_usort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_array_user_key_compare, false); + php_usort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_array_user_key_compare, false, NULL); } /* }}} */ diff --git a/ext/standard/tests/array/sort/packed_integer_sort.phpt b/ext/standard/tests/array/sort/packed_integer_sort.phpt new file mode 100644 index 000000000000..1d2458fe5439 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_integer_sort.phpt @@ -0,0 +1,65 @@ +--TEST-- +Packed integer sort and rsort at insertion sort and pivot selection boundaries +--FILE-- + 1, 2 => 2, 0 => 3]); +arsort($values); +check($values, [0 => 3, 2 => 2, 1 => 1]); +ksort($values); +check($values, [3, 1, 2]); +krsort($values); +check($values, [2 => 2, 1 => 1, 0 => 3]); +echo "other comparators: OK\n"; +?> +--EXPECT-- +size 2: OK +size 16: OK +size 17: OK +size 32: OK +size 63: OK +size 64: OK +size 65: OK +size 1024: OK +size 1025: OK +other comparators: OK diff --git a/ext/standard/tests/array/sort/packed_integer_sort_iterators.phpt b/ext/standard/tests/array/sort/packed_integer_sort_iterators.phpt new file mode 100644 index 000000000000..34d5a90e7c33 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_integer_sort_iterators.phpt @@ -0,0 +1,47 @@ +--TEST-- +Sorting packed integer arrays during by-reference foreach preserves iterator positions +--FILE-- + &$value) { + $visited[] = [$key, $value]; + if ($first) { + $first = false; + if ($removeCurrent) { + // Remove the referenced element while keeping the iterator active. + array_shift($values); + foreach (array_keys($values) as $index) { + check(ReflectionReference::fromArrayElement($values, $index), null); + } + } + $sort($values); + } + } + unset($value); + $count = $size + (int) !$removeCurrent; + $expected = $sort === 'sort' ? range(1, $count) : range($count, 1); + $expectedVisited = [[0, $size + 1]]; + for ($index = $removeCurrent ? 0 : 1; $index < $count; $index++) { + $expectedVisited[] = [$index, $expected[$index]]; + } + check($visited, $expectedVisited); + check($values, $expected); + } + echo "$sort size $size: OK\n"; +} +?> +--EXPECT-- +sort size 3: OK +sort size 64: OK +rsort size 3: OK +rsort size 64: OK diff --git a/ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt b/ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt new file mode 100644 index 000000000000..3bdac3706198 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt @@ -0,0 +1,67 @@ +--TEST-- +Packed integer sorting resets array metadata and preserves copy on write +--FILE-- + 123]); + + $values = [11, 22]; + unset($values[0]); + $sort($values); + $values[] = 123; + check($values, [22, 123]); + echo "$sort: OK\n"; +} +?> +--EXPECT-- +sort: OK +rsort: OK diff --git a/ext/standard/tests/array/sort/packed_zval_sort.phpt b/ext/standard/tests/array/sort/packed_zval_sort.phpt new file mode 100644 index 000000000000..c6f0ea635157 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort.phpt @@ -0,0 +1,111 @@ +--TEST-- +Packed zval sort and rsort preserve all comparison modes and stable value ordering +--INI-- +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- + 'asort', 'rsort' => 'arsort'] as $sort => $referenceSort) { + $original = serialize($input); + $expected = $input; + $referenceSort($expected, $flags); + $values = $input; + end($values); + check($sort($values, $flags), true); + check($values, array_values($expected)); + check(serialize($input), $original); + check(key($values), 0); + $values[] = 'appended'; + check(array_key_last($values), count($input)); + } +} +class SortableString { + public function __construct(public string $value) {} + public function __toString(): string { return $this->value; } +} +enum SortableEnum { case A; case B; } + +setlocale(LC_COLLATE, 'C'); +foreach ([2, 16, 17, 32, 63, 64, 65, 128, 1025] as $size) { + $values = []; + $strings = []; + $objects = []; + for ($i = 0; $i < $size; $i++) { + $values[] = [1, 1.0, '1', '01', -0.0, 0, null, false, true, 2.5, '10', '2', INF, -INF][$i % 14]; + $strings[] = ['a10', 'A10', 'a2', 'A2', '1', '01'][$i % 6]; + $objects[] = new SortableString($strings[$i]); + } + $valuesWithHoles = $values; + $stringsWithHoles = $strings; + unset($valuesWithHoles[0], $stringsWithHoles[0]); + foreach ([SORT_REGULAR, SORT_REGULAR | SORT_FLAG_CASE, 12345, + SORT_NUMERIC, SORT_NUMERIC | SORT_FLAG_CASE, + SORT_STRING, SORT_STRING | SORT_FLAG_CASE, SORT_NATURAL, + SORT_NATURAL | SORT_FLAG_CASE, SORT_LOCALE_STRING, + SORT_LOCALE_STRING | SORT_FLAG_CASE] as $flags) { + verify($values, $flags); + verify($strings, $flags); + verify($valuesWithHoles, $flags); + verify($stringsWithHoles, $flags); + if (($flags & ~SORT_FLAG_CASE) !== SORT_NUMERIC) { + verify($objects, $flags); + } + } + echo "size $size: OK\n"; +} + +$values = []; +for ($i = 0; $i < 128; $i++) { + $values[] = [SortableEnum::A, SortableEnum::B, null, 1, ['v' => 1], ['v' => 2]][$i % 6]; +} +verify($values, SORT_REGULAR); +unset($values[1], $values[60], $values[127]); +verify($values, SORT_REGULAR); +echo "enums, nested arrays and holes: OK\n"; + +foreach (['sort' => 'asort', 'rsort' => 'arsort'] as $sort => $referenceSort) { + $references = array_fill(0, 64, '01'); + $values = []; + foreach ($references as &$value) { + $values[] = &$value; + } + unset($value); + $expected = $values; + $referenceSort($expected, SORT_REGULAR); + $sort($values, SORT_REGULAR); + foreach ($references as $index => &$value) { + $value = $index; + } + unset($value); + check($values, array_values($expected)); + check($values, range(0, 63)); +} +echo "stable references: OK\n"; + +$first = fopen('php://memory', 'r+'); +$second = fopen('php://memory', 'r+'); +$values = []; +for ($i = 0; $i < 64; $i++) $values[] = $i % 2 ? $first : $second; +foreach ([SORT_REGULAR, SORT_NUMERIC, SORT_STRING] as $flags) verify($values, $flags); +fclose($first); +fclose($second); +echo "resources: OK\n"; +?> +--EXPECT-- +size 2: OK +size 16: OK +size 17: OK +size 32: OK +size 63: OK +size 64: OK +size 65: OK +size 128: OK +size 1025: OK +enums, nested arrays and holes: OK +stable references: OK +resources: OK diff --git a/ext/standard/tests/array/sort/packed_zval_sort_callback_read.phpt b/ext/standard/tests/array/sort/packed_zval_sort_callback_read.phpt new file mode 100644 index 000000000000..85a6d411ebc0 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_callback_read.phpt @@ -0,0 +1,47 @@ +--TEST-- +Implicit sorting callbacks can read packed array indices at every array size +--INI-- +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +value; + } +} + +setlocale(LC_COLLATE, 'C'); +foreach ([2, 16, 17, 32, 63, 64, 65, 1025] as $size) { + foreach (['sort', 'rsort'] as $sort) { + foreach ([SORT_STRING, SORT_STRING | SORT_FLAG_CASE, SORT_NATURAL, + SORT_NATURAL | SORT_FLAG_CASE, SORT_LOCALE_STRING] as $flags) { + $values = []; + for ($i = $size; $i > 0; $i--) $values[] = new ReadDuringSort($i); + $reads = 0; + ReadDuringSort::$read = function () use (&$values, &$reads, $size) { + if (!$values[0] instanceof ReadDuringSort + || !$values[$size - 1] instanceof ReadDuringSort) { + throw new Exception('Missing numeric index during comparison'); + } + $reads++; + }; + $sort($values, $flags); + if (!$reads || !array_is_list($values)) throw new Exception('No reads or invalid keys'); + ReadDuringSort::$read = null; + } + } + echo "size $size: OK\n"; +} +?> +--EXPECT-- +size 2: OK +size 16: OK +size 17: OK +size 32: OK +size 63: OK +size 64: OK +size 65: OK +size 1025: OK diff --git a/ext/standard/tests/array/sort/packed_zval_sort_internal_pointer.phpt b/ext/standard/tests/array/sort/packed_zval_sort_internal_pointer.phpt new file mode 100644 index 000000000000..3688a5730704 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_internal_pointer.phpt @@ -0,0 +1,38 @@ +--TEST-- +Packed sorting relocates the internal pointer before invoking comparison callbacks +--FILE-- +value; + } +} + +foreach (['sort', 'rsort'] as $sort) { + $values = []; + foreach ([5, 4, 3, 2, 1] as $value) { + $values[] = new PointerReader($value); + } + next($values); + next($values); + next($values); + unset($values[0], $values[1]); + PointerReader::$read = static function () use (&$values, $sort) { + echo "$sort during: ", current($values)->value, "\n"; + }; + $sort($values, SORT_STRING); + echo "$sort after: ", current($values)->value, "\n"; +} +?> +--EXPECT-- +sort during: 2 +sort after: 1 +rsort during: 2 +rsort after: 3 diff --git a/ext/standard/tests/array/sort/packed_zval_sort_iterators_holes.phpt b/ext/standard/tests/array/sort/packed_zval_sort_iterators_holes.phpt new file mode 100644 index 000000000000..cc9000a13e26 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_iterators_holes.phpt @@ -0,0 +1,68 @@ +--TEST-- +Packed sorting compacts holes without losing by-reference foreach positions +--FILE-- + &$value) { + $visited[] = "$key=>$value"; + if ($first) { + $first = false; + sort($values); + } +} +unset($value); +echo implode(" ", $visited), "\n"; + +$values = [9, 8, 7, 6, 5, 4]; +unset($values[0], $values[1]); +$visited = []; +$first = true; +foreach ($values as $key => &$value) { + $visited[] = "$key=>$value"; + if ($first) { + $first = false; + rsort($values); + } +} +unset($value); +echo implode(" ", $visited), "\n"; + +// A cursor at the old end must pick up an append after compaction. +$values = [3, 2, 1]; +unset($values[0], $values[1]); +$visited = []; +$first = true; +foreach ($values as $key => &$value) { + $visited[] = "$key=>$value"; + if ($first) { + $first = false; + sort($values); + $values[] = 9; + } +} +unset($value); +echo implode(" ", $visited), "\n"; + +// Unsetting the current reference leaves an all-integer packed array with a hole. +$values = [3, 2, 1]; +$visited = []; +$first = true; +foreach ($values as $key => &$value) { + $visited[] = "$key=>$value"; + if ($first) { + $first = false; + unset($values[$key]); + sort($values); + } +} +unset($value); +echo implode(" ", $visited), "\n"; +?> +--EXPECT-- +3=>2 1=>2 +2=>7 1=>6 2=>5 3=>4 +2=>1 1=>9 +0=>3 0=>1 1=>2 diff --git a/ext/standard/tests/array/sort/packed_zval_sort_nested_iterators.phpt b/ext/standard/tests/array/sort/packed_zval_sort_nested_iterators.phpt new file mode 100644 index 000000000000..fa69d1feebd0 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_nested_iterators.phpt @@ -0,0 +1,26 @@ +--TEST-- +Packed sorting relocates cursors on both a hole and the next defined element +--FILE-- + &$inner) { + $visited[] = "$key=>$inner"; + if ($inner === 11) { + // The outer cursor is at hole 1; the inner cursor is at value 2. + unset($values[0], $values[1]); + $sort($values); + } + } + unset($inner); + break; + } + unset($outer); + echo "$sort: ", implode(' ', $visited), "\n"; +} +?> +--EXPECT-- +sort: 0=>10 1=>11 0=>12 1=>13 2=>14 +rsort: 0=>10 1=>11 0=>14 1=>13 2=>12 diff --git a/ext/standard/tests/array/sort/packed_zval_sort_numeric_stability.phpt b/ext/standard/tests/array/sort/packed_zval_sort_numeric_stability.phpt new file mode 100644 index 000000000000..36c54f54265d --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_numeric_stability.phpt @@ -0,0 +1,34 @@ +--TEST-- +Packed numeric sorting preserves the order of distinct integers that compare as equal doubles +--SKIPIF-- + +--FILE-- + 'asort', 'rsort' => 'arsort'] as $sort => $referenceSort) { + $actual = $values; + $sort($actual, SORT_NUMERIC); + if ($actual !== $values) throw new Exception('Equal numeric values reordered'); + + $limits = [...$values, PHP_INT_MAX, PHP_INT_MAX - 1, PHP_INT_MIN, PHP_INT_MIN + 1]; + foreach ([SORT_NUMERIC, SORT_REGULAR] as $flags) { + $expected = $actual = $limits; + $referenceSort($expected, $flags); + $sort($actual, $flags); + if ($actual !== array_values($expected)) throw new Exception('Integer limits reordered'); + } + } + echo "size $size: OK\n"; +} +?> +--EXPECT-- +size 2: OK +size 16: OK +size 17: OK +size 32: OK +size 64: OK +size 1025: OK diff --git a/ext/standard/tests/array/sort/packed_zval_sort_recursive.phpt b/ext/standard/tests/array/sort/packed_zval_sort_recursive.phpt new file mode 100644 index 000000000000..d7a50df83707 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_recursive.phpt @@ -0,0 +1,39 @@ +--TEST-- +Packed zval sorting handles recursive array references and collection after sorting +--FILE-- + +--EXPECT-- +int(64) +bool(true) +bool(true) +bool(true) +int(64) +bool(true) +int(64) +bool(true) +bool(true) +bool(true) +int(64) +bool(true) diff --git a/ext/standard/tests/array/sort/packed_zval_sort_reentrancy.phpt b/ext/standard/tests/array/sort/packed_zval_sort_reentrancy.phpt new file mode 100644 index 000000000000..5f8a8d4fac20 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_reentrancy.phpt @@ -0,0 +1,111 @@ +--TEST-- +Packed zval sorting keeps its buffer alive when conversions modify the sorted variable +--FILE-- +value; + } + public function __destruct() { self::$destroyed++; } +} +function makeValues(int $size) { + $values = []; + for ($i = $size; $i > 0; $i--) $values[] = new ReentrantString($i); + return $values; +} + +foreach ([2, 16, 32, 64, 1025] as $size) { + foreach (['sort', 'rsort'] as $sort) { + $values = makeValues($size); + $original = $values; + ReentrantString::$action = function () use (&$values) { $values[] = 'appended'; }; + check($sort($values, SORT_STRING), true); + check($values, [...$original, 'appended']); + unset($values, $original); + + $values = makeValues($size); + ReentrantString::$action = function () use (&$values) { $values = ['replacement']; }; + check($sort($values, SORT_STRING), true); + check($values, ['replacement']); + + $values = makeValues($size); + $weak = WeakReference::create($values[0]); + ReentrantString::$destroyed = 0; + ReentrantString::$action = function () use (&$values) { $values = null; gc_collect_cycles(); }; + check($sort($values, SORT_STRING), true); + check($values, null); + check($weak->get(), null); + check(ReentrantString::$destroyed, $size); + + $values = makeValues($size); + $expected = $values; + $expected[0] = 'changed'; + $referenceSort = $sort === 'sort' ? 'asort' : 'arsort'; + $referenceSort($expected, SORT_STRING); + ReentrantString::$action = function () use (&$values, $sort) { + $values[0] = 'changed'; + $sort($values, SORT_STRING); + }; + check($sort($values, SORT_STRING), true); + check($values, array_values($expected)); + unset($values, $expected); + + $values = makeValues($size); + $ids = array_map(spl_object_id(...), $values); + ReentrantString::$action = function () { throw new RuntimeException('conversion'); }; + try { + $sort($values, SORT_STRING); + throw new Exception('Missing exception'); + } catch (RuntimeException $e) { + check($e->getMessage(), 'conversion'); + } + $resultIds = array_map(spl_object_id(...), $values); + sort($ids); + sort($resultIds); + check($resultIds, $ids); + check(array_is_list($values), true); + unset($values); + + $values = array_fill(0, $size, []); + $first = true; + set_error_handler(function () use (&$values, &$first) { + if ($first) { + $first = false; + $values = ['error handler']; + gc_collect_cycles(); + } + return true; + }); + try { + check($sort($values, SORT_STRING), true); + } finally { + restore_error_handler(); + } + check($first, false); + check($values, ['error handler']); + echo "$size $sort: OK\n"; + } +} +?> +--EXPECT-- +2 sort: OK +2 rsort: OK +16 sort: OK +16 rsort: OK +32 sort: OK +32 rsort: OK +64 sort: OK +64 rsort: OK +1025 sort: OK +1025 rsort: OK diff --git a/ext/standard/tests/array/sort/packed_zval_sort_scalar_reentrancy.phpt b/ext/standard/tests/array/sort/packed_zval_sort_scalar_reentrancy.phpt new file mode 100644 index 000000000000..f2a7dc1bbd83 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_scalar_reentrancy.phpt @@ -0,0 +1,40 @@ +--TEST-- +Packed scalar sorting keeps its buffer alive when NAN coercion invokes an error handler +--FILE-- + +--EXPECT-- +2 sort: OK +2 rsort: OK +17 sort: OK +17 rsort: OK +1025 sort: OK +1025 rsort: OK diff --git a/ext/standard/tests/array/sort/packed_zval_sort_stability.phpt b/ext/standard/tests/array/sort/packed_zval_sort_stability.phpt new file mode 100644 index 000000000000..493cdbef2404 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_sort_stability.phpt @@ -0,0 +1,41 @@ +--TEST-- +Packed sorting preserves stable comparisons for references and non-integers +--FILE-- + +--EXPECT-- +sort: OK +rsort: OK diff --git a/ext/standard/tests/array/sort/packed_zval_usort.phpt b/ext/standard/tests/array/sort/packed_zval_usort.phpt new file mode 100644 index 000000000000..e781366ac0a1 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_zval_usort.phpt @@ -0,0 +1,60 @@ +--TEST-- +Packed usort preserves stability, input isolation, holes, exceptions and nested callbacks +--FILE-- + $a[0] <=> $b[0]); + check(usort($values, function ($a, $b) use (&$values, $original) { + check($values, $original); + return $a[0] <=> $b[0]; + }), true); + check($values, array_values($expected)); + $values[] = 'appended'; + check(array_key_last($values), count($original)); + } + + $values = $input; + $expected = $input; + uasort($expected, fn($a, $b) => $a[0] <=> $b[0]); + $first = true; + usort($values, function ($a, $b) use (&$values, &$first) { + if ($first) { + $first = false; + usort($values, fn($a, $b) => $b[1] <=> $a[1]); + } + return $a[0] <=> $b[0]; + }); + check($values, array_values($expected)); + + $values = $input; + try { + usort($values, function () { throw new RuntimeException('comparison'); }); + throw new Exception('Missing exception'); + } catch (RuntimeException $e) { + check($e->getMessage(), 'comparison'); + } + check(array_is_list($values), true); + $ids = array_column($values, 1); + sort($ids); + check($ids, range(0, $size - 1)); + check(count($input), $size); + echo "size $size: OK\n"; +} +?> +--EXPECT-- +size 2: OK +size 16: OK +size 17: OK +size 32: OK +size 64: OK +size 1025: OK