Skip to content

Commit 9026917

Browse files
committed
ext/standard/array.c: make enum SORT_REGULAR ordering deterministic
- Compare backed enums via their stored backing values so SORT_REGULAR’s common path no longer fetches and compares case names; unit enums still fall back to case-name ordering, with object handles as the deterministic tie-breaker - Add ext/standard/tests/array/sort/sort_enum_stability.phpt to ensure both unit and backed enums produce the same sorted order regardless of access order
1 parent 5f79902 commit 9026917

File tree

2 files changed

+121
-4
lines changed

2 files changed

+121
-4
lines changed

ext/standard/array.c

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <string.h>
2929
#include "zend_globals.h"
3030
#include "zend_interfaces.h"
31+
#include "zend_enum.h"
3132
#include "php_array.h"
3233
#include "basic_functions.h"
3334
#include "php_string.h"
@@ -137,13 +138,61 @@ static zend_always_inline int php_array_compare_enum_zvals(zval *lhs, zval *rhs)
137138
const bool rhs_enum = php_array_is_enum_zval(rhs);
138139

139140
if (lhs_enum && rhs_enum) {
140-
if (Z_OBJ_P(lhs) == Z_OBJ_P(rhs)) {
141+
zend_object *lhs_obj = Z_OBJ_P(lhs);
142+
zend_object *rhs_obj = Z_OBJ_P(rhs);
143+
144+
if (lhs_obj == rhs_obj) {
141145
return 0;
142146
}
143147

144-
uintptr_t lhs_ptr = (uintptr_t) Z_OBJ_P(lhs);
145-
uintptr_t rhs_ptr = (uintptr_t) Z_OBJ_P(rhs);
146-
return lhs_ptr < rhs_ptr ? -1 : 1;
148+
if (lhs_obj->ce != rhs_obj->ce) {
149+
return zend_compare_non_numeric_strings(lhs_obj->ce->name, rhs_obj->ce->name);
150+
}
151+
152+
if (lhs_obj->ce->enum_backing_type != IS_UNDEF) {
153+
zval *lhs_value = zend_enum_fetch_case_value(lhs_obj);
154+
zval *rhs_value = zend_enum_fetch_case_value(rhs_obj);
155+
156+
if (lhs_obj->ce->enum_backing_type == IS_LONG) {
157+
zend_long lhs_long = Z_LVAL_P(lhs_value);
158+
zend_long rhs_long = Z_LVAL_P(rhs_value);
159+
if (lhs_long != rhs_long) {
160+
return lhs_long < rhs_long ? -1 : 1;
161+
}
162+
} else {
163+
ZEND_ASSERT(lhs_obj->ce->enum_backing_type == IS_STRING);
164+
zend_string *lhs_str = Z_STR_P(lhs_value);
165+
zend_string *rhs_str = Z_STR_P(rhs_value);
166+
if (lhs_str != rhs_str) {
167+
zend_ulong lhs_hash = ZSTR_HASH(lhs_str);
168+
zend_ulong rhs_hash = ZSTR_HASH(rhs_str);
169+
if (lhs_hash != rhs_hash) {
170+
return lhs_hash < rhs_hash ? -1 : 1;
171+
}
172+
int cmp = zend_compare_non_numeric_strings(lhs_str, rhs_str);
173+
if (cmp != 0) {
174+
return cmp;
175+
}
176+
}
177+
}
178+
}
179+
180+
zend_string *lhs_case = Z_STR_P(zend_enum_fetch_case_name(lhs_obj));
181+
zend_string *rhs_case = Z_STR_P(zend_enum_fetch_case_name(rhs_obj));
182+
if (lhs_case != rhs_case) {
183+
zend_ulong lhs_hash = ZSTR_HASH(lhs_case);
184+
zend_ulong rhs_hash = ZSTR_HASH(rhs_case);
185+
if (lhs_hash != rhs_hash) {
186+
return lhs_hash < rhs_hash ? -1 : 1;
187+
}
188+
int cmp = zend_compare_non_numeric_strings(lhs_case, rhs_case);
189+
if (cmp != 0) {
190+
return cmp;
191+
}
192+
}
193+
194+
/* Should not happen for userland enums, but keep ordering deterministic for transitivity. */
195+
return lhs_obj->handle < rhs_obj->handle ? -1 : 1;
147196
}
148197

149198
return lhs_enum ? 1 : -1;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
SORT_REGULAR produces stable ordering for enums regardless of access order
3+
--FILE--
4+
<?php
5+
enum UnitEnumExample {
6+
case Hearts;
7+
case Spades;
8+
case Clubs;
9+
case Diamonds;
10+
}
11+
12+
enum BackedEnumExample: string {
13+
case Alpha = 'alpha';
14+
case Beta = 'beta';
15+
case Gamma = 'gamma';
16+
case Delta = 'delta';
17+
}
18+
19+
function build_cases(string $enumClass, array $order): array {
20+
$cases = [];
21+
foreach ($order as $case) {
22+
$cases[] = constant($enumClass . "::$case");
23+
}
24+
return $cases;
25+
}
26+
27+
function sorted_unit(array $order): array {
28+
$cases = build_cases(UnitEnumExample::class, $order);
29+
sort($cases, SORT_REGULAR);
30+
return array_map(fn(UnitEnumExample $c) => $c->name, $cases);
31+
}
32+
33+
function sorted_backed(array $order): array {
34+
$cases = build_cases(BackedEnumExample::class, $order);
35+
sort($cases, SORT_REGULAR);
36+
return array_map(fn(BackedEnumExample $c) => $c->value, $cases);
37+
}
38+
39+
$unitOrders = [
40+
['Hearts', 'Spades', 'Clubs', 'Diamonds'],
41+
['Diamonds', 'Clubs', 'Spades', 'Hearts'],
42+
['Spades', 'Hearts', 'Diamonds', 'Clubs'],
43+
];
44+
45+
$unitBaseline = sorted_unit($unitOrders[0]);
46+
foreach ($unitOrders as $idx => $order) {
47+
if (sorted_unit($order) !== $unitBaseline) {
48+
echo "Unit enum order mismatch for permutation $idx\n";
49+
}
50+
}
51+
52+
$backedOrders = [
53+
['Alpha', 'Beta', 'Gamma', 'Delta'],
54+
['Delta', 'Gamma', 'Beta', 'Alpha'],
55+
['Beta', 'Alpha', 'Delta', 'Gamma'],
56+
];
57+
58+
$backedBaseline = sorted_backed($backedOrders[0]);
59+
foreach ($backedOrders as $idx => $order) {
60+
if (sorted_backed($order) !== $backedBaseline) {
61+
echo "Backed enum order mismatch for permutation $idx\n";
62+
}
63+
}
64+
65+
echo "done\n";
66+
?>
67+
--EXPECT--
68+
done

0 commit comments

Comments
 (0)