Skip to content

Commit 5295b23

Browse files
committed
Change iteration behavior to be relative to original position
Make Deque iteration account for calls to shift/unshift tracking the position of the front of the Deque. Calls to shift/unshift will do the following: - Increase/Decrease the value returned by the iterator's key() by the number of elements added/removed to/from the front of the Deque. (`$deque[$iteratorKey] === $iteratorValue` at the time the key and value are returned). - Repeated calls to shift will cause valid() to return false if the iterator's position ends up before the start of the Deque at the time iteration resumes. - They will not cause the remaining values to be iterated over more than once or skipped.
1 parent 8fb127a commit 5295b23

File tree

3 files changed

+146
-15
lines changed

3 files changed

+146
-15
lines changed

ext/spl/spl_deque.c

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,19 @@ zend_class_entry *spl_ce_Deque;
7171
static const zval empty_entry_list[1];
7272

7373
typedef struct _spl_deque_entries {
74+
/* The number of elements in the Deque. */
7475
size_t size;
7576
/* One less than a power of two (the capacity) */
7677
size_t mask;
78+
/* The offset of the start of the deque in the circular buffer. */
7779
size_t offset;
80+
/*
81+
* This is a counter which increases when adding elements to the front and decreases when removing elements from the front.
82+
* This is used so that iteration works as expected when the front position is modified during foreach.
83+
*
84+
* This is a uint64_t to work consistently and avoid easily overflowing 4 billion on 32-bit builds.
85+
*/
86+
uint64_t iteration_offset;
7887
/** This is a circular buffer with an offset, size, and capacity(mask + 1) */
7988
zval *circular_buffer;
8089
} spl_deque_entries;
@@ -116,7 +125,8 @@ typedef struct _spl_deque {
116125
/* Used by InternalIterator returned by Deque->getIterator() */
117126
typedef struct _spl_deque_it {
118127
zend_object_iterator intern;
119-
zend_long current;
128+
/* This starts at iteration_offset and increases by one when next() is called. */
129+
uint64_t current;
120130
} spl_deque_it;
121131

122132
static zend_always_inline void spl_deque_push_back(spl_deque *intern, zval *value);
@@ -153,29 +163,43 @@ static bool spl_deque_entries_uninitialized(spl_deque_entries *array)
153163
return array->circular_buffer == NULL;
154164
}
155165

156-
/* Based n zend_hash_check_size. Finds the next largest power of 2. */
166+
/* Based on zend_hash_check_size which supports 32-bit integers. Finds the next largest power of 2. */
157167
static inline size_t spl_deque_next_pow2_capacity(size_t nSize) {
158168
if (nSize < DEQUE_MIN_CAPACITY) {
159169
return DEQUE_MIN_CAPACITY;
160170
}
171+
/* Note that for values such as 63 or 31 of the form ((2^n) - 1),
172+
* subtracting and xor are the same things for numbers in the range of 0 to the max. */
161173
#ifdef ZEND_WIN32
162174
unsigned long index;
175+
#if SIZEOF_SIZE_T > 4
176+
if (BitScanReverse64(&index, nSize - 1)) {
177+
return 0x2u << ((63 - index) ^ 0x3f);
178+
}
179+
#else
163180
if (BitScanReverse(&index, nSize - 1)) {
164181
return 0x2u << ((31 - index) ^ 0x1f);
165-
} else {
166-
/* nSize is ensured to be in the valid range, fall back to it
167-
rather than using an undefined bis scan result. */
168-
return nSize;
169182
}
183+
#endif
184+
/* nSize is ensured to be in the valid range, fall back to it
185+
* rather than using an undefined bit scan result. */
186+
return nSize;
170187
#elif (defined(__GNUC__) || __has_builtin(__builtin_clz)) && defined(PHP_HAVE_BUILTIN_CLZ)
188+
#if SIZEOF_SIZE_T > SIZEOF_INT
189+
return 0x2u << (__builtin_clzl(nSize - 1) ^ (sizeof(long) * 8 - 1));
190+
#else
171191
return 0x2u << (__builtin_clz(nSize - 1) ^ 0x1f);
192+
#endif
172193
#else
173194
nSize -= 1;
174195
nSize |= (nSize >> 1);
175196
nSize |= (nSize >> 2);
176197
nSize |= (nSize >> 4);
177198
nSize |= (nSize >> 8);
178199
nSize |= (nSize >> 16);
200+
#if SIZEOF_SIZE_T > 4
201+
nSize |= (nSize >> 32);
202+
#endif
179203
return nSize + 1;
180204
#endif
181205
}
@@ -184,6 +208,7 @@ static void spl_deque_entries_init_from_array(spl_deque_entries *array, zend_arr
184208
{
185209
zend_long size = zend_hash_num_elements(values);
186210
array->offset = 0;
211+
array->iteration_offset = 0;
187212
array->size = 0; /* reset size and capacity in case emalloc() fails */
188213
array->mask = 0;
189214
if (size > 0) {
@@ -212,6 +237,7 @@ static void spl_deque_entries_init_from_traversable(spl_deque_entries *array, ze
212237
zend_long size = 0, capacity = 0;
213238
array->size = 0;
214239
array->offset = 0;
240+
array->iteration_offset = 0;
215241
array->circular_buffer = NULL;
216242
zval *circular_buffer = NULL;
217243
zval tmp_obj;
@@ -274,6 +300,7 @@ static void spl_deque_entries_copy_ctor(spl_deque_entries *to, const spl_deque_e
274300
to->size = 0; /* reset size in case emalloc() fails */
275301
to->mask = 0;
276302
to->offset = 0;
303+
to->iteration_offset = 0;
277304
if (!size) {
278305
to->circular_buffer = (zval *)empty_entry_list;
279306
return;
@@ -321,6 +348,7 @@ static void spl_deque_entries_dtor(spl_deque_entries *array)
321348
array->offset = 0;
322349
array->size = 0;
323350
array->mask = 0;
351+
/* Changing iteration_offset doesn't matter */
324352
do {
325353
if (p == end) {
326354
p = circular_buffer;
@@ -504,6 +532,7 @@ PHP_METHOD(Deque, __construct)
504532

505533
if (iterable == NULL) {
506534
intern->array.offset = 0;
535+
intern->array.iteration_offset = 0;
507536
intern->array.size = 0;
508537
intern->array.mask = 0;
509538
intern->array.circular_buffer = (zval *)empty_entry_list;
@@ -535,15 +564,17 @@ static void spl_deque_it_dtor(zend_object_iterator *iter)
535564

536565
static void spl_deque_it_rewind(zend_object_iterator *iter)
537566
{
538-
((spl_deque_it*)iter)->current = 0;
567+
const spl_deque *object = Z_DEQUE_P(&iter->data);
568+
((spl_deque_it*)iter)->current = object->array.iteration_offset;
539569
}
540570

541571
static int spl_deque_it_valid(zend_object_iterator *iter)
542572
{
543573
const spl_deque_it *iterator = (spl_deque_it*)iter;
544574
const spl_deque *object = Z_DEQUE_P(&iter->data);
575+
const size_t offset = iterator->current - object->array.iteration_offset;
545576

546-
if (iterator->current >= 0 && (zend_ulong) iterator->current < object->array.size) {
577+
if (offset < object->array.size) {
547578
return SUCCESS;
548579
}
549580

@@ -566,8 +597,9 @@ static zval *spl_deque_it_get_current_data(zend_object_iterator *iter)
566597
{
567598
const spl_deque_it *iterator = (spl_deque_it*)iter;
568599
spl_deque *object = Z_DEQUE_P(&iter->data);
600+
const uint64_t offset = iterator->current - object->array.iteration_offset;
569601

570-
zval *data = spl_deque_read_offset_helper(object, iterator->current);
602+
zval *data = spl_deque_read_offset_helper(object, offset);
571603

572604
if (UNEXPECTED(data == NULL)) {
573605
return &EG(uninitialized_zval);
@@ -580,8 +612,8 @@ static void spl_deque_it_get_current_key(zend_object_iterator *iter, zval *key)
580612
{
581613
const spl_deque_it *iterator = (spl_deque_it*)iter;
582614
const spl_deque *object = Z_DEQUE_P(&iter->data);
615+
const uint64_t offset = iterator->current - object->array.iteration_offset;
583616

584-
size_t offset = iterator->current;
585617
if (offset >= object->array.size) {
586618
ZVAL_NULL(key);
587619
} else {
@@ -1020,6 +1052,7 @@ PHP_METHOD(Deque, unshift)
10201052
}
10211053

10221054
spl_deque *intern = Z_DEQUE_P(ZEND_THIS);
1055+
intern->array.iteration_offset -= argc; /* The front moved backwards by the number of elements */
10231056
size_t old_size = intern->array.size;
10241057
const size_t new_size = old_size + argc;
10251058
size_t mask = intern->array.mask;
@@ -1105,6 +1138,7 @@ PHP_METHOD(Deque, shift)
11051138
}
11061139

11071140
intern->array.size--;
1141+
intern->array.iteration_offset++; /* The front moved forward */
11081142
const size_t old_offset = intern->array.offset;
11091143
const size_t old_mask = intern->array.mask;
11101144
intern->array.offset++;

ext/spl/tests/Deque/foreach.phpt

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
--TEST--
2+
Deque modification during foreach
3+
--FILE--
4+
<?php
5+
6+
/** Creates a reference-counted version of a literal string to test reference counting. */
7+
function mut(string $s) {
8+
$s[0] = $s[0];
9+
return $s;
10+
}
11+
12+
echo "Test push/unshift\n";
13+
$deque = new Deque(['a', 'b']);
14+
foreach ($deque as $key => $value) {
15+
if (strlen($value) === 1) {
16+
$deque->push("${value}_");
17+
$deque->unshift("_${value}");
18+
}
19+
printf("Key: %s Value: %s\n", var_export($key, true), var_export($value, true));
20+
}
21+
var_dump($deque);
22+
echo "Test shift\n";
23+
foreach ($deque as $key => $value) {
24+
echo "Shifting $key $value\n";
25+
var_dump($deque->shift());
26+
}
27+
28+
echo "Test shift out of bounds\n";
29+
$deque = new Deque([mut('a1'), mut('b1'), mut('c1')]);
30+
foreach ($deque as $key => $value) {
31+
$deque->shift();
32+
$deque->shift();
33+
echo "Saw $key: $value\n";
34+
// iteration stops early because iteration position is now out of bounds.
35+
}
36+
var_dump($deque);
37+
38+
echo "Test iteration behavior\n";
39+
$deque = new Deque([mut('a1'), mut('a2')]);
40+
$it = $deque->getIterator();
41+
echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n";
42+
$deque->shift();
43+
// invalid, outside the range of the deque
44+
echo json_encode(['valid' => $it->valid(), 'key' => $it->key()]), "\n";
45+
$it->next();
46+
echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n";
47+
$deque->unshift('a', 'b');
48+
unset($deque);
49+
echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n";
50+
51+
?>
52+
--EXPECT--
53+
Test push/unshift
54+
Key: 0 Value: 'a'
55+
Key: 2 Value: 'b'
56+
Key: 4 Value: 'a_'
57+
Key: 5 Value: 'b_'
58+
object(Deque)#1 (6) {
59+
[0]=>
60+
string(2) "_b"
61+
[1]=>
62+
string(2) "_a"
63+
[2]=>
64+
string(1) "a"
65+
[3]=>
66+
string(1) "b"
67+
[4]=>
68+
string(2) "a_"
69+
[5]=>
70+
string(2) "b_"
71+
}
72+
Test shift
73+
Shifting 0 _b
74+
string(2) "_b"
75+
Shifting 0 _a
76+
string(2) "_a"
77+
Shifting 0 a
78+
string(1) "a"
79+
Shifting 0 b
80+
string(1) "b"
81+
Shifting 0 a_
82+
string(2) "a_"
83+
Shifting 0 b_
84+
string(2) "b_"
85+
Test shift out of bounds
86+
Saw 0: a1
87+
object(Deque)#2 (1) {
88+
[0]=>
89+
string(2) "c1"
90+
}
91+
Test iteration behavior
92+
{"valid":true,"key":0,"value":"a1"}
93+
{"valid":false,"key":null}
94+
{"valid":true,"key":0,"value":"a2"}
95+
{"valid":true,"key":2,"value":"a2"}

ext/spl/tests/Deque/iterator.phpt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ function expect_throws(Closure $cb): void {
1212
}
1313
}
1414

15-
// Iterators are associated with an index (as in offsetGet), not a position in the deque.
15+
// Iterators are associated with a position in the deque relative to the front of the deque *when iteration started*. key() returns the distance from the current start of the deque, or null.
1616
$dq = new Deque([new stdClass(), strtoupper('test')]);
1717
$it = $dq->getIterator();
1818
var_dump($it->key());
1919
var_dump($it->current());
2020
var_dump($it->next());
2121
var_dump($it->valid());
22+
echo "After shift\n";
2223
$dq->shift();
2324
var_dump($it->key());
24-
expect_throws(fn() => $it->current());
25+
var_dump($it->current());
2526
var_dump($it->valid());
2627
$dq->shift();
2728
var_dump($it->key()); // null for invalid iterator
@@ -38,9 +39,10 @@ object(stdClass)#2 (0) {
3839
}
3940
NULL
4041
bool(true)
41-
NULL
42-
Caught OutOfBoundsException: Index out of range
43-
bool(false)
42+
After shift
43+
int(0)
44+
string(4) "TEST"
45+
bool(true)
4446
NULL
4547
Caught OutOfBoundsException: Index out of range
4648
bool(false)

0 commit comments

Comments
 (0)