Skip to content

Commit 0250dac

Browse files
committed
ext/standard: Add SORT_STRICT flag for type-safe array sorting
RFC: https://wiki.php.net/rfc/sort_strict
1 parent 9c81b52 commit 0250dac

13 files changed

+1007
-1
lines changed

ext/standard/array.c

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,225 @@ static int php_array_data_compare_string_locale_unstable_i(Bucket *f, Bucket *s)
359359
}
360360
/* }}} */
361361

362+
/* Returns the position of a PHP type in the total ordering used by SORT_STRICT. */
363+
static zend_always_inline int php_array_type_order(uint8_t type)
364+
{
365+
switch (type) {
366+
case IS_NULL: return 0;
367+
case IS_FALSE: return 1;
368+
case IS_TRUE: return 2;
369+
case IS_LONG: return 3;
370+
case IS_DOUBLE: return 4;
371+
case IS_STRING: return 5;
372+
case IS_ARRAY: return 6;
373+
case IS_OBJECT: return 7;
374+
case IS_RESOURCE: return 8;
375+
EMPTY_SWITCH_DEFAULT_CASE()
376+
}
377+
}
378+
379+
static zend_always_inline int php_array_compare_strings(zend_string *s1, zend_string *s2)
380+
{
381+
if (s1 == s2) {
382+
return 0;
383+
}
384+
size_t len1 = ZSTR_LEN(s1);
385+
size_t len2 = ZSTR_LEN(s2);
386+
size_t min_len = len1 < len2 ? len1 : len2;
387+
int cmp = memcmp(ZSTR_VAL(s1), ZSTR_VAL(s2), min_len);
388+
if (cmp != 0) {
389+
return cmp < 0 ? -1 : 1;
390+
}
391+
return ZEND_THREEWAY_COMPARE(len1, len2);
392+
}
393+
394+
static zend_always_inline int php_array_sort_compare_strict(zval *op1, zval *op2);
395+
static int php_array_sort_compare_objects_strict(zval *o1, zval *o2);
396+
397+
/* Wrapper for zend_hash_compare callback. php_array_sort_compare_strict is
398+
* zend_always_inline for performance, so we need this addressable wrapper. */
399+
static int php_array_data_compare_strict_callback(zval *op1, zval *op2)
400+
{
401+
return php_array_sort_compare_strict(op1, op2);
402+
}
403+
404+
static int php_array_sort_compare_symbol_tables_strict(HashTable *ht1, HashTable *ht2)
405+
{
406+
if (ht1 == ht2) {
407+
return 0;
408+
}
409+
410+
GC_TRY_ADDREF(ht1);
411+
GC_TRY_ADDREF(ht2);
412+
413+
int ret = zend_hash_compare(ht1, ht2, (compare_func_t)php_array_data_compare_strict_callback, 0);
414+
415+
GC_TRY_DTOR_NO_REF(ht1);
416+
GC_TRY_DTOR_NO_REF(ht2);
417+
418+
return ret;
419+
}
420+
421+
static int php_array_sort_compare_objects_strict(zval *o1, zval *o2)
422+
{
423+
ZEND_ASSERT(Z_TYPE_P(o1) == IS_OBJECT && Z_TYPE_P(o2) == IS_OBJECT);
424+
425+
zend_object *zobj1 = Z_OBJ_P(o1);
426+
zend_object *zobj2 = Z_OBJ_P(o2);
427+
428+
if (zobj1->ce != zobj2->ce) {
429+
return zobj1->ce > zobj2->ce ? 1 : -1;
430+
}
431+
432+
if (zobj1 == zobj2) {
433+
return 0;
434+
}
435+
436+
if (zobj1->ce->ce_flags & ZEND_ACC_ENUM) {
437+
return zobj1->handle > zobj2->handle ? 1 : -1;
438+
}
439+
440+
if (Z_OBJ_HT_P(o1)->compare && Z_OBJ_HT_P(o1)->compare != zend_std_compare_objects) {
441+
return Z_OBJ_HT_P(o1)->compare(o1, o2);
442+
}
443+
444+
/* Compare declared properties directly when no dynamic properties exist */
445+
if (!zobj1->properties && !zobj2->properties
446+
&& !zend_object_is_lazy(zobj1) && !zend_object_is_lazy(zobj2)) {
447+
zend_property_info *info;
448+
int i, ret;
449+
450+
if (!zobj1->ce->default_properties_count) {
451+
return 0;
452+
}
453+
454+
if (UNEXPECTED(Z_IS_RECURSIVE_P(o1))) {
455+
zend_throw_error(NULL, "Nesting level too deep - recursive dependency?");
456+
return ZEND_UNCOMPARABLE;
457+
}
458+
Z_PROTECT_RECURSION_P(o1);
459+
460+
GC_ADDREF(zobj1);
461+
GC_ADDREF(zobj2);
462+
463+
ret = 0;
464+
for (i = 0; i < zobj1->ce->default_properties_count; i++) {
465+
zval *p1, *p2;
466+
467+
info = zobj1->ce->properties_info_table[i];
468+
469+
if (!info) {
470+
continue;
471+
}
472+
473+
p1 = OBJ_PROP(zobj1, info->offset);
474+
p2 = OBJ_PROP(zobj2, info->offset);
475+
476+
if (Z_TYPE_P(p1) != IS_UNDEF) {
477+
if (Z_TYPE_P(p2) != IS_UNDEF) {
478+
ret = php_array_sort_compare_strict(p1, p2);
479+
if (ret != 0) {
480+
break;
481+
}
482+
} else {
483+
ret = 1;
484+
break;
485+
}
486+
} else if (Z_TYPE_P(p2) != IS_UNDEF) {
487+
ret = -1;
488+
break;
489+
}
490+
}
491+
492+
Z_UNPROTECT_RECURSION_P(o1);
493+
OBJ_RELEASE(zobj1);
494+
OBJ_RELEASE(zobj2);
495+
return ret;
496+
}
497+
498+
/* Dynamic properties exist: compare via property hash tables */
499+
GC_ADDREF(zobj1);
500+
GC_ADDREF(zobj2);
501+
502+
int ret = php_array_sort_compare_symbol_tables_strict(
503+
zend_std_get_properties_ex(zobj1),
504+
zend_std_get_properties_ex(zobj2)
505+
);
506+
507+
OBJ_RELEASE(zobj1);
508+
OBJ_RELEASE(zobj2);
509+
510+
return ret;
511+
}
512+
513+
static zend_always_inline int php_array_sort_compare_strict(zval *op1, zval *op2)
514+
{
515+
ZVAL_DEREF(op1);
516+
ZVAL_DEREF(op2);
517+
518+
uint8_t t1 = Z_TYPE_P(op1);
519+
uint8_t t2 = Z_TYPE_P(op2);
520+
521+
if (t1 == t2) {
522+
switch (t1) {
523+
case IS_LONG:
524+
return ZEND_THREEWAY_COMPARE(Z_LVAL_P(op1), Z_LVAL_P(op2));
525+
526+
case IS_STRING:
527+
return php_array_compare_strings(Z_STR_P(op1), Z_STR_P(op2));
528+
529+
case IS_DOUBLE:
530+
/* Per IEEE 754 totalOrder, NaN sorts after all other values */
531+
if (UNEXPECTED(zend_isnan(Z_DVAL_P(op1)))) {
532+
return zend_isnan(Z_DVAL_P(op2)) ? 0 : 1;
533+
}
534+
if (UNEXPECTED(zend_isnan(Z_DVAL_P(op2)))) {
535+
return -1;
536+
}
537+
return ZEND_THREEWAY_COMPARE(Z_DVAL_P(op1), Z_DVAL_P(op2));
538+
539+
case IS_ARRAY:
540+
return php_array_sort_compare_symbol_tables_strict(Z_ARRVAL_P(op1), Z_ARRVAL_P(op2));
541+
542+
case IS_OBJECT:
543+
return php_array_sort_compare_objects_strict(op1, op2);
544+
545+
case IS_NULL:
546+
case IS_FALSE:
547+
case IS_TRUE:
548+
return 0;
549+
550+
case IS_RESOURCE:
551+
return ZEND_THREEWAY_COMPARE(Z_RES_P(op1)->handle, Z_RES_P(op2)->handle);
552+
553+
EMPTY_SWITCH_DEFAULT_CASE()
554+
}
555+
}
556+
557+
/* Types differ: order by type hierarchy */
558+
/* Special case: IS_FALSE (2) and IS_TRUE (3) are both bool, so compare by value */
559+
if ((t1 | 1) == IS_TRUE && (t2 | 1) == IS_TRUE) {
560+
return t1 > t2 ? 1 : -1;
561+
}
562+
return php_array_type_order(t1) > php_array_type_order(t2) ? 1 : -1;
563+
}
564+
565+
static zend_always_inline int php_array_data_compare_strict_unstable_i(Bucket *f, Bucket *s)
566+
{
567+
return php_array_sort_compare_strict(&f->val, &s->val);
568+
}
569+
570+
static zend_always_inline int php_array_key_compare_strict_unstable_i(Bucket *f, Bucket *s)
571+
{
572+
if (f->key == NULL && s->key == NULL) {
573+
return ZEND_THREEWAY_COMPARE((zend_long)f->h, (zend_long)s->h);
574+
}
575+
if (f->key && s->key) {
576+
return php_array_compare_strings(f->key, s->key);
577+
}
578+
return f->key ? 1 : -1;
579+
}
580+
362581
DEFINE_SORT_VARIANTS(key_compare);
363582
DEFINE_SORT_VARIANTS(key_compare_numeric);
364583
DEFINE_SORT_VARIANTS(key_compare_string_case);
@@ -371,6 +590,8 @@ DEFINE_SORT_VARIANTS(data_compare_string);
371590
DEFINE_SORT_VARIANTS(data_compare_string_locale);
372591
DEFINE_SORT_VARIANTS(natural_compare);
373592
DEFINE_SORT_VARIANTS(natural_case_compare);
593+
DEFINE_SORT_VARIANTS(key_compare_strict);
594+
DEFINE_SORT_VARIANTS(data_compare_strict);
374595

375596
static bucket_compare_func_t php_get_key_compare_func(zend_long sort_type)
376597
{
@@ -395,6 +616,9 @@ static bucket_compare_func_t php_get_key_compare_func(zend_long sort_type)
395616
case PHP_SORT_LOCALE_STRING:
396617
return php_array_key_compare_string_locale;
397618

619+
case PHP_SORT_STRICT:
620+
return php_array_key_compare_strict;
621+
398622
case PHP_SORT_REGULAR:
399623
default:
400624
return php_array_key_compare;
@@ -425,6 +649,9 @@ static bucket_compare_func_t php_get_key_reverse_compare_func(zend_long sort_typ
425649
case PHP_SORT_LOCALE_STRING:
426650
return php_array_reverse_key_compare_string_locale;
427651

652+
case PHP_SORT_STRICT:
653+
return php_array_reverse_key_compare_strict;
654+
428655
case PHP_SORT_REGULAR:
429656
default:
430657
return php_array_reverse_key_compare;
@@ -455,6 +682,9 @@ static bucket_compare_func_t php_get_data_compare_func(zend_long sort_type) /* {
455682
case PHP_SORT_LOCALE_STRING:
456683
return php_array_data_compare_string_locale;
457684

685+
case PHP_SORT_STRICT:
686+
return php_array_data_compare_strict;
687+
458688
case PHP_SORT_REGULAR:
459689
default:
460690
return php_array_data_compare;
@@ -485,6 +715,9 @@ static bucket_compare_func_t php_get_data_reverse_compare_func(zend_long sort_ty
485715
case PHP_SORT_LOCALE_STRING:
486716
return php_array_reverse_data_compare_string_locale;
487717

718+
case PHP_SORT_STRICT:
719+
return php_array_reverse_data_compare_strict;
720+
488721
case PHP_SORT_REGULAR:
489722
default:
490723
return php_array_reverse_data_compare;
@@ -543,6 +776,14 @@ static bucket_compare_func_t php_get_data_compare_func_unstable(zend_long sort_t
543776
}
544777
break;
545778

779+
case PHP_SORT_STRICT:
780+
if (reverse) {
781+
return php_array_reverse_data_compare_strict_unstable;
782+
} else {
783+
return php_array_data_compare_strict_unstable;
784+
}
785+
break;
786+
546787
case PHP_SORT_REGULAR:
547788
default:
548789
if (reverse) {
@@ -6003,6 +6244,7 @@ PHP_FUNCTION(array_multisort)
60036244
case PHP_SORT_STRING:
60046245
case PHP_SORT_NATURAL:
60056246
case PHP_SORT_LOCALE_STRING:
6247+
case PHP_SORT_STRICT:
60066248
/* flag allowed here */
60076249
if (parse_state[MULTISORT_TYPE] == 1) {
60086250
/* Save the flag and make sure then next arg is not the current flag. */

ext/standard/basic_functions.stub.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@
8181
* @cvalue PHP_SORT_NATURAL
8282
*/
8383
const SORT_NATURAL = UNKNOWN;
84+
/**
85+
* @var int
86+
* @cvalue PHP_SORT_STRICT
87+
*/
88+
const SORT_STRICT = UNKNOWN;
8489
/**
8590
* @var int
8691
* @cvalue PHP_SORT_FLAG_CASE

ext/standard/basic_functions_arginfo.h

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/standard/php_array.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ PHPAPI bool php_array_pick_keys(php_random_algo_with_state engine, zval *input,
5454
#define PHP_SORT_ASC 4
5555
#define PHP_SORT_LOCALE_STRING 5
5656
#define PHP_SORT_NATURAL 6
57+
#define PHP_SORT_STRICT 7
5758
#define PHP_SORT_FLAG_CASE 8
5859

5960
#define PHP_COUNT_NORMAL 0

0 commit comments

Comments
 (0)