Skip to content

Commit 1e27ead

Browse files
committed
Fix iterator relocation at the current element during rehash
When one iterator points at a hole and another at the next live bucket, the relocation loop must include the iterator at that bucket's original position. Otherwise it is moved to the following bucket's destination, causing a nested by-reference foreach to skip an element. Add regression coverage for packed-to-hash conversion and compaction of a full mixed table.
1 parent 4365896 commit 1e27ead

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ PHP NEWS
77
n_scale. (Ilia Alshanetsky)
88

99
- Core:
10+
. Fixed incorrect foreach iterator positions when compacting arrays with
11+
holes. (Weilin Du)
1012
. Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or
1113
next() call on the inner generator). (iliaal)
1214
. Fixed bug GH-23232 (lone namespace separator asks the autoloader for an
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Rehashing updates iterators at both a hole and the next defined element
3+
--FILE--
4+
<?php
5+
function test(array $values, $firstKey, $secondKey, $newKey, int $newValue): void {
6+
$outerVisits = [];
7+
$innerVisits = [];
8+
$first = true;
9+
foreach ($values as $outerKey => &$outerValue) {
10+
$outerVisits[] = "$outerKey=>$outerValue";
11+
if ($first) {
12+
$first = false;
13+
foreach ($values as $innerKey => &$innerValue) {
14+
$innerVisits[] = "$innerKey=>$innerValue";
15+
if ($innerValue === 11) {
16+
// The outer cursor is at a hole, the inner at the next value.
17+
unset($values[$firstKey], $values[$secondKey]);
18+
$values[$newKey] = $newValue;
19+
}
20+
}
21+
unset($innerValue);
22+
}
23+
}
24+
unset($outerValue);
25+
echo 'outer: ', implode(' ', $outerVisits), "\n";
26+
echo 'inner: ', implode(' ', $innerVisits), "\n";
27+
}
28+
29+
// Adding a string key converts packed storage and compacts its holes.
30+
test([10, 11, 12, 13, 14], 0, 1, 'new', 15);
31+
32+
// Inserting into a full mixed table compacts its holes without growing it.
33+
test(['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13,
34+
'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17], 'a', 'b', 'i', 18);
35+
?>
36+
--EXPECT--
37+
outer: 0=>10 2=>12 3=>13 4=>14 new=>15
38+
inner: 0=>10 1=>11 2=>12 3=>13 4=>14 new=>15
39+
outer: a=>10 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
40+
inner: a=>10 b=>11 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18

Zend/zend_hash.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1412,7 +1412,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht)
14121412
do {
14131413
zend_hash_iterators_update(ht, iter_pos, j);
14141414
iter_pos = zend_hash_iterators_lower_pos(ht, iter_pos + 1);
1415-
} while (iter_pos < i);
1415+
} while (iter_pos <= i);
14161416
}
14171417
q++;
14181418
j++;

0 commit comments

Comments
 (0)