From f2fa8fa2b70c56a7bbacc21fbbeb1215049ee307 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Sun, 19 Sep 2021 09:35:26 -0400 Subject: [PATCH 1/3] RFC: Add `final class Collections\Deque` to PHP This has lower memory usage and better performance than SplDoublyLinkedList for push/pop operations. The API is as follows: ```php namespace Collections; /** * A double-ended queue (Typically abbreviated as Deque, pronounced "deck", like "cheque") * represented internally as a circular buffer. * * This has much lower memory usage than SplDoublyLinkedList or its subclasses (SplStack, SplStack), * and operations are significantly faster than SplDoublyLinkedList. * * See https://en.wikipedia.org/wiki/Double-ended_queue * * This supports amortized constant time pushing and popping onto the start (i.e. start, first) * or back (i.e. end, last) of the Deque. * * Method naming is based on https://www.php.net/spldoublylinkedlist * and on array_push/pop/unshift/shift/ and array_key_first/array_key_last. */ final class Deque implements IteratorAggregate, Countable, JsonSerializable, ArrayAccess { /** Construct the Deque from the values of the Traversable/array, ignoring keys */ public function __construct(iterable $iterator = []) {} /** * Returns an iterator that accounts for calls to shift/unshift tracking the position of the start 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 start 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. */ public function getIterator(): \InternalIterator {} /** Returns the number of elements in the Deque. */ public function count(): int {} /** Returns true if there are 0 elements in the Deque. */ public function isEmpty(): bool {} /** Removes all elements from the Deque. */ public function clear(): void {} public function __serialize(): array {} public function __unserialize(array $data): void {} /** Construct the Deque from the values of the array, ignoring keys */ public static function __set_state(array $array): Deque {} /** Appends value(s) to the end of the Deque. */ public function push(mixed ...$values): void {} /** Prepends value(s) to the start of the Deque. */ public function unshift(mixed ...$values): void {} /** * Pops a value from the end of the Deque. * @throws \UnderflowException if the Deque is empty */ public function pop(): mixed {} /** * Shifts a value from the start of the Deque. * @throws \UnderflowException if the Deque is empty */ public function shift(): mixed {} /** * Peeks at the value at the start of the Deque. * @throws \UnderflowException if the Deque is empty */ public function first(): mixed {} /** * Peeks at the value at the end of the Deque. * @throws \UnderflowException if the Deque is empty */ public function last(): mixed {} /** * Returns a list of the elements from the start to the end. */ public function toArray(): array {} // Must be mixed for compatibility with ArrayAccess /** * Insert 0 or more values at the given offset of the Deque. * @throws \OutOfBoundsException if the value of $offset is not within the bounds of this Deque. */ public function insert(int $offset, mixed ...$values): void {} /** * Returns the value at offset (int)$offset (relative to the start of the Deque) * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this vector */ public function offsetGet(mixed $offset): mixed {} /** * Returns true if `0 <= (int)$offset && (int)$offset < $this->count(). */ public function offsetExists(mixed $offset): bool {} /** * Sets the value at offset $offset (relative to the start of the Deque) to $value * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this vector */ public function offsetSet(mixed $offset, mixed $value): void {} /** * Removes the value at (int)$offset from the deque. * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this Deque. */ public function offsetUnset(mixed $offset): void {} /** * This is JSON serialized as a JSON array with elements from the start to the end. */ public function jsonSerialize(): array {} } ``` Earlier work on the implementation can be found at https://github.com/TysonAndre/pecl-teds (though `Teds\Deque` hasn't been updated with new names yet) This was originally based on spl_fixedarray.c and previous work I did on an RFC. Notable features of `Deque` - Significantly lower memory usage and better performance than `SplDoublyLinkedList` - Amortized constant time operations for push/pop/unshift/shift. - Reclaims memory when roughly a quarter of the capacity is used, unlike array, which never releases allocated capacity https://www.npopov.com/2014/12/22/PHPs-new-hashtable-implementation.html > One problem with the current implementation is that arData never shrinks > (unless explicitly told to). So if you create an array with a few million > elements and remove them afterwards, the array will still take a lot of > memory. We should probably half the arData size if utilization falls below a > certain level. For long-running applications when the maximum count of Deque is larger than the average count, this may be a concern. - Adds functionality that cannot be implemented nearly efficiently in an array. For example, shifting a single element onto an array (and making it first in iteration order) with `array_shift` would take linear time, because all elements in the array would need to be moved to make room for the first one - Support `$deque[] = $element`, like ArrayObject. - Having this functionality in php itself rather than a third party extension would encourage wider adoption of this --- ext/collections/collections_deque.c | 1307 +++++++++++++++++ ext/collections/collections_deque.h | 24 + ext/collections/collections_deque.stub.php | 112 ++ ext/collections/collections_deque_arginfo.h | 129 ++ .../collections_internaliterator.h | 58 + ext/collections/collections_util.c | 41 + ext/collections/collections_util.h | 97 ++ ext/collections/config.m4 | 5 + ext/collections/config.w32 | 5 + ext/collections/php_collections.c | 64 + ext/collections/php_collections.h | 31 + ext/collections/tests/Deque/Deque.phpt | 44 + ext/collections/tests/Deque/aggregate.phpt | 17 + ext/collections/tests/Deque/arrayCast.phpt | 30 + ext/collections/tests/Deque/clear.phpt | 25 + ext/collections/tests/Deque/clone.phpt | 20 + .../tests/Deque/exceptionhandler.phpt | 36 + ext/collections/tests/Deque/foreach.phpt | 98 ++ ext/collections/tests/Deque/isEmpty.phpt | 15 + ext/collections/tests/Deque/iterator.phpt | 50 + ext/collections/tests/Deque/offsetGet.phpt | 67 + .../tests/Deque/offsetGetShifted.phpt | 54 + ext/collections/tests/Deque/offsetSet.phpt | 37 + ext/collections/tests/Deque/popFront.phpt | 13 + ext/collections/tests/Deque/popMany.phpt | 40 + ext/collections/tests/Deque/pushFront.phpt | 22 + .../tests/Deque/push_multiple.phpt | 18 + ext/collections/tests/Deque/push_pop.phpt | 53 + .../tests/Deque/push_pop_both.phpt | 45 + .../tests/Deque/reinit_forbidden.phpt | 29 + .../tests/Deque/serialization.phpt | 34 + ext/collections/tests/Deque/set_state.phpt | 83 ++ ext/collections/tests/Deque/shift.phpt | 13 + ext/collections/tests/Deque/toArray.phpt | 27 + ext/collections/tests/Deque/top.phpt | 44 + ext/collections/tests/Deque/traversable.phpt | 101 ++ ext/collections/tests/Deque/unserialize.phpt | 17 + ext/collections/tests/Deque/unshift.phpt | 22 + .../tests/Deque/var_export_recursion.phpt | 38 + ext/spl/config.m4 | 2 +- ext/spl/config.w32 | 2 +- ext/spl/spl_fixedarray.c | 32 +- ext/spl/spl_util.h | 53 + 43 files changed, 3021 insertions(+), 33 deletions(-) create mode 100644 ext/collections/collections_deque.c create mode 100644 ext/collections/collections_deque.h create mode 100644 ext/collections/collections_deque.stub.php create mode 100644 ext/collections/collections_deque_arginfo.h create mode 100644 ext/collections/collections_internaliterator.h create mode 100644 ext/collections/collections_util.c create mode 100644 ext/collections/collections_util.h create mode 100644 ext/collections/config.m4 create mode 100644 ext/collections/config.w32 create mode 100644 ext/collections/php_collections.c create mode 100644 ext/collections/php_collections.h create mode 100644 ext/collections/tests/Deque/Deque.phpt create mode 100644 ext/collections/tests/Deque/aggregate.phpt create mode 100644 ext/collections/tests/Deque/arrayCast.phpt create mode 100644 ext/collections/tests/Deque/clear.phpt create mode 100644 ext/collections/tests/Deque/clone.phpt create mode 100644 ext/collections/tests/Deque/exceptionhandler.phpt create mode 100644 ext/collections/tests/Deque/foreach.phpt create mode 100644 ext/collections/tests/Deque/isEmpty.phpt create mode 100644 ext/collections/tests/Deque/iterator.phpt create mode 100644 ext/collections/tests/Deque/offsetGet.phpt create mode 100644 ext/collections/tests/Deque/offsetGetShifted.phpt create mode 100644 ext/collections/tests/Deque/offsetSet.phpt create mode 100644 ext/collections/tests/Deque/popFront.phpt create mode 100644 ext/collections/tests/Deque/popMany.phpt create mode 100644 ext/collections/tests/Deque/pushFront.phpt create mode 100644 ext/collections/tests/Deque/push_multiple.phpt create mode 100644 ext/collections/tests/Deque/push_pop.phpt create mode 100644 ext/collections/tests/Deque/push_pop_both.phpt create mode 100644 ext/collections/tests/Deque/reinit_forbidden.phpt create mode 100644 ext/collections/tests/Deque/serialization.phpt create mode 100644 ext/collections/tests/Deque/set_state.phpt create mode 100644 ext/collections/tests/Deque/shift.phpt create mode 100644 ext/collections/tests/Deque/toArray.phpt create mode 100644 ext/collections/tests/Deque/top.phpt create mode 100644 ext/collections/tests/Deque/traversable.phpt create mode 100644 ext/collections/tests/Deque/unserialize.phpt create mode 100644 ext/collections/tests/Deque/unshift.phpt create mode 100644 ext/collections/tests/Deque/var_export_recursion.phpt create mode 100644 ext/spl/spl_util.h diff --git a/ext/collections/collections_deque.c b/ext/collections/collections_deque.c new file mode 100644 index 0000000000000..1d8c82c3da902 --- /dev/null +++ b/ext/collections/collections_deque.c @@ -0,0 +1,1307 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Tyson Andre | + +----------------------------------------------------------------------+ +*/ + +/* This is based on spl_fixedarray.c but has lower overhead (when size is known) and is more efficient to add and remove elements from the start of the Deque */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "php.h" +#include "php_ini.h" +#include "ext/standard/info.h" +#include "zend_exceptions.h" +#include "zend_interfaces.h" + +#include "php_collections.h" +#include "collections_deque_arginfo.h" +#include "collections_deque.h" +#include "collections_internaliterator.h" +#include "collections_util.h" +// #include "ext/collections/collections_functions.h" +#include "ext/spl/spl_exceptions.h" +#include "ext/spl/spl_iterators.h" +#include "ext/json/php_json.h" +#include "ext/spl/spl_util.h" + +#include + +static const zval collections_empty_entry_list[1]; + +/* Common functionality */ +#define CONVERT_OFFSET_TO_LONG_OR_THROW(index, zv) do { \ + if (Z_TYPE_P(offset_zv) != IS_LONG) { \ + index = spl_offset_convert_to_long(offset_zv); \ + if (UNEXPECTED(EG(exception))) { \ + return; \ + } \ + } else { \ + index = Z_LVAL_P(offset_zv); \ + } \ +} while(0) + +#define CONVERT_OFFSET_TO_LONG_OR_THROW_RETURN_NULLPTR(index, zv) do { \ + if (Z_TYPE_P(offset_zv) != IS_LONG) { \ + index = spl_offset_convert_to_long(offset_zv); \ + if (UNEXPECTED(EG(exception))) { \ + return NULL; \ + } \ + } else { \ + index = Z_LVAL_P(offset_zv); \ + } \ +} while(0) + +static void collections_throw_invalid_sequence_index_exception(void) +{ + zend_throw_exception(spl_ce_OutOfBoundsException, "Index out of range", 0); +} + +#define COLLECTIONS_THROW_INVALID_SEQUENCE_INDEX_EXCEPTION() do { collections_throw_invalid_sequence_index_exception(); RETURN_THROWS(); } while (0) +/* End common functionality */ + +#define COLLECTIONS_DEQUE_MIN_CAPACITY 4 +#define COLLECTIONS_DEQUE_MIN_MASK (COLLECTIONS_DEQUE_MIN_CAPACITY - 1) + +static zend_always_inline size_t collections_deque_next_pow2_capacity(size_t nSize) { + return collections_next_pow2_capacity(nSize, COLLECTIONS_DEQUE_MIN_CAPACITY); +} + +zend_object_handlers collections_handler_Deque; +zend_class_entry *collections_ce_Deque; + +typedef struct _collections_deque_entries { + /** This is a circular buffer with an offset, size, and capacity(mask + 1) */ + zval *circular_buffer; + /* The number of elements in the Deque. */ + uint32_t size; + /* One less than a power of two (the capacity) */ + uint32_t mask; + collections_intrusive_dllist active_iterators; + /* The offset of the start of the deque in the circular buffer. */ + uint32_t offset; +} collections_deque_entries; + +/* Is this 0 or a power of 2? */ +static zend_always_inline bool collections_is_valid_uint32_capacity(uint32_t size) { + return (size & (size-1)) == 0; +} + +static zend_always_inline uint32_t collections_deque_entries_get_capacity(const collections_deque_entries *array) +{ + return array->mask ? array->mask + 1 : 0; +} + +static zend_always_inline void collections_deque_entries_try_shrink_capacity(collections_deque_entries *array, uint32_t old_size); + +#if ZEND_DEBUG +static void DEBUG_ASSERT_CONSISTENT_DEQUE(const collections_deque_entries *array) { + const uint32_t capacity = collections_deque_entries_get_capacity(array); + ZEND_ASSERT(array->size <= capacity); + ZEND_ASSERT(array->offset < capacity || capacity == 0); + ZEND_ASSERT(array->mask == 0 || (array->circular_buffer != NULL && array->circular_buffer != collections_empty_entry_list)); + ZEND_ASSERT(collections_is_valid_uint32_capacity(capacity)); + ZEND_ASSERT(array->circular_buffer != NULL || ((array->size == 0 && array->offset == 0) || capacity == 0)); +} +#else +#define DEBUG_ASSERT_CONSISTENT_DEQUE(array) do {} while(0) +#endif + +static zend_always_inline zval* collections_deque_get_entry_at_offset(const collections_deque_entries *array, uint32_t offset) { + DEBUG_ASSERT_CONSISTENT_DEQUE(array); + ZEND_ASSERT(offset < array->size); + return &array->circular_buffer[(array->offset + offset) & array->mask]; +} + +typedef struct _collections_deque { + collections_deque_entries array; + zend_object std; +} collections_deque; + +/* Used by InternalIterator returned by Deque->getIterator() */ +typedef struct _collections_deque_it { + zend_object_iterator intern; + collections_intrusive_dllist_node dllist_node; + uint32_t current; +} collections_deque_it; + +static zend_always_inline void collections_deque_entries_push_back(collections_deque_entries *array, zval *value); +static void collections_deque_entries_raise_capacity(collections_deque_entries *array, const size_t new_capacity); +static void collections_deque_entries_shrink_capacity(collections_deque_entries *array, const uint32_t new_capacity); + +static zend_always_inline collections_deque *collections_deque_from_object(zend_object *obj) +{ + return (collections_deque*)((char*)(obj) - XtOffsetOf(collections_deque, std)); +} + +static zend_always_inline zend_object *collections_deque_to_object(collections_deque_entries *array) +{ + return (zend_object*)((char*)(array) + XtOffsetOf(collections_deque, std)); +} + +static zend_always_inline collections_deque_it *collections_deque_it_from_node(collections_intrusive_dllist_node *node) +{ + return (collections_deque_it*)((char*)(node) - XtOffsetOf(collections_deque_it, dllist_node)); +} + +#define collections_deque_entries_from_object(obj) (&collections_deque_from_object((obj))->array) + +#define Z_DEQUE_P(zv) collections_deque_from_object(Z_OBJ_P((zv))) +#define Z_DEQUE_ENTRIES_P(zv) collections_deque_entries_from_object(Z_OBJ_P((zv))) + + +static zend_always_inline bool collections_deque_entries_empty_capacity(const collections_deque_entries *array) +{ + DEBUG_ASSERT_CONSISTENT_DEQUE(array); + return array->mask == 0; +} + +static zend_always_inline bool collections_deque_entries_uninitialized(const collections_deque_entries *array) +{ + DEBUG_ASSERT_CONSISTENT_DEQUE(array); + return array->circular_buffer == NULL; +} + +static void collections_deque_entries_init_from_array(collections_deque_entries *array, zend_array *values) +{ + zend_long size = zend_hash_num_elements(values); + array->offset = 0; + array->size = 0; /* reset size and capacity in case emalloc() fails */ + array->mask = 0; + if (size > 0) { + zval *val; + zval *circular_buffer; + int i = 0; + + const uint32_t capacity = collections_deque_next_pow2_capacity(size); + array->circular_buffer = circular_buffer = safe_emalloc(capacity, sizeof(zval), 0); + array->size = size; + array->mask = capacity - 1; + ZEND_HASH_FOREACH_VAL(values, val) { + ZEND_ASSERT(i < size); + /* This circular buffer is being initialized with an array->offset of 0. */ + ZVAL_COPY_DEREF(&circular_buffer[i], val); + i++; + } ZEND_HASH_FOREACH_END(); + } else { + array->circular_buffer = (zval *)collections_empty_entry_list; + } +} + +static void collections_deque_entries_init_from_traversable(collections_deque_entries *array, zend_object *obj) +{ + zend_class_entry *ce = obj->ce; + zend_object_iterator *iter; + uint32_t size = 0; + size_t capacity = 0; + array->size = 0; + array->offset = 0; + array->circular_buffer = NULL; + zval *circular_buffer = NULL; + zval tmp_obj; + ZVAL_OBJ(&tmp_obj, obj); + iter = ce->get_iterator(ce, &tmp_obj, 0); + + if (UNEXPECTED(EG(exception))) { + return; + } + + const zend_object_iterator_funcs *funcs = iter->funcs; + + if (funcs->rewind) { + funcs->rewind(iter); + if (UNEXPECTED(EG(exception))) { + goto cleanup_iter; + } + } + + while (funcs->valid(iter) == SUCCESS) { + if (UNEXPECTED(EG(exception))) { + break; + } + zval *value = funcs->get_current_data(iter); + if (UNEXPECTED(EG(exception))) { + break; + } + + if (size >= capacity) { + /* TODO: Could use countable and get_count handler to estimate the size of the array to allocate but there's no guarantee count is supported */ + if (circular_buffer) { + capacity *= 2; + circular_buffer = safe_erealloc(circular_buffer, capacity, sizeof(zval), 0); + } else { + capacity = 4; + circular_buffer = safe_emalloc(capacity, sizeof(zval), 0); + } + } + ZVAL_COPY_DEREF(&circular_buffer[size], value); + size++; + + iter->index++; + funcs->move_forward(iter); + if (UNEXPECTED(EG(exception))) { + break; + } + } + + array->size = size; + array->mask = capacity > 0 ? capacity - 1 : 0; + array->circular_buffer = circular_buffer; +cleanup_iter: + if (iter) { + zend_iterator_dtor(iter); + } +} + +static void collections_deque_entries_copy_ctor(collections_deque_entries *to, const collections_deque_entries *from) +{ + zend_long size = from->size; + to->size = 0; /* reset size in case emalloc() fails */ + to->mask = 0; + to->offset = 0; + if (!size) { + to->circular_buffer = (zval *)collections_empty_entry_list; + return; + } + + const uint32_t capacity = collections_deque_next_pow2_capacity(size); + to->circular_buffer = safe_emalloc(size, sizeof(zval), 0); + to->size = size; + to->mask = capacity - 1; + ZEND_ASSERT(to->mask <= from->mask); + ZEND_ASSERT(from->mask > 0); + + // account for offsets + zval *const from_buffer_start = from->circular_buffer; + zval *from_begin = &from_buffer_start[from->offset]; + zval *const from_end = &from_buffer_start[from->mask + 1]; + + zval *p_dest = to->circular_buffer; + zval *p_end = p_dest + size; + do { + if (from_begin == from_end) { + from_begin = from_buffer_start; + } + ZVAL_COPY(p_dest, from_begin); + from_begin++; + p_dest++; + } while (p_dest < p_end); +} + +/* Destructs and frees contents but not the array itself. + * If you want to re-use the array then you need to re-initialize it. + */ +static void collections_deque_entries_dtor(collections_deque_entries *array) +{ + if (collections_deque_entries_empty_capacity(array)) { + return; + } + uint32_t remaining = array->size; + zval *const circular_buffer = array->circular_buffer; + if (remaining > 0) { + zval *const end = circular_buffer + array->mask + 1; + zval *p = circular_buffer + array->offset; + ZEND_ASSERT(p < end); + array->circular_buffer = NULL; + array->offset = 0; + array->size = 0; + array->mask = 0; + do { + if (p == end) { + p = circular_buffer; + } + zval_ptr_dtor(p); + p++; + remaining--; + } while (remaining > 0); + } + efree(circular_buffer); +} + +static HashTable* collections_deque_get_gc(zend_object *obj, zval **table, int *n) +{ + collections_deque *intern = collections_deque_from_object(obj); + + if (!intern->array.mask) { + ZEND_ASSERT(intern->array.size == 0); + *n = 0; + return obj->properties; + } + const uint32_t size = intern->array.size; + const uint32_t capacity = intern->array.mask + 1; + const uint32_t offset = intern->array.offset; + zval * const circular_buffer = intern->array.circular_buffer; + if (capacity - offset >= size) { + *table = &circular_buffer[offset]; + *n = (int)size; + return obj->properties; + } + + // Based on spl_dllist.c + zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); + for (uint32_t i = offset; i < capacity; i++) { + zend_get_gc_buffer_add_zval(gc_buffer, &circular_buffer[i]); + } + + for (uint32_t i = 0, len = offset + size - capacity; i < len; i++) { + zend_get_gc_buffer_add_zval(gc_buffer, &circular_buffer[len]); + } + + /* This replaces table and n. */ + zend_get_gc_buffer_use(gc_buffer, table, n); + return obj->properties; +} + +static zend_array* collections_deque_entries_to_refcounted_array(const collections_deque_entries *array); + +static HashTable* collections_deque_get_properties_for(zend_object *obj, zend_prop_purpose purpose) +{ + collections_deque_entries *array = &collections_deque_from_object(obj)->array; + if (!array->size && !obj->properties) { + /* Similar to ext/ffi/ffi.c zend_fake_get_properties */ + /* debug_zval_dump DEBUG purpose requires null or a refcounted array. */ + return NULL; + } + switch (purpose) { + case ZEND_PROP_PURPOSE_VAR_EXPORT: /* In php 8.3+, can return brand new arrays for var_export/debug_zval_dump and have infinite recursion work */ + case ZEND_PROP_PURPOSE_DEBUG: + case ZEND_PROP_PURPOSE_ARRAY_CAST: + case ZEND_PROP_PURPOSE_SERIALIZE: + /* Return null or a brand new array that will be garbage collected, to avoid increasing memory usage after the call finishes */ + if (!array->size) { + return NULL; + } + return collections_deque_entries_to_refcounted_array(array); + case ZEND_PROP_PURPOSE_JSON: /* jsonSerialize(alias of toArray) is used instead. */ + default: + ZEND_UNREACHABLE(); + return NULL; + } +} + +static void collections_deque_free_storage(zend_object *object) +{ + collections_deque *intern = collections_deque_from_object(object); + collections_deque_entries_dtor(&intern->array); + zend_object_std_dtor(&intern->std); +} + +static zend_object *collections_deque_new_ex(zend_class_entry *class_type, zend_object *orig, bool clone_orig) +{ + collections_deque *intern; + + intern = zend_object_alloc(sizeof(collections_deque), class_type); + /* This is a final class */ + ZEND_ASSERT(class_type == collections_ce_Deque); + + zend_object_std_init(&intern->std, class_type); + object_properties_init(&intern->std, class_type); + intern->std.handlers = &collections_handler_Deque; + + if (orig && clone_orig) { + collections_deque *other = collections_deque_from_object(orig); + collections_deque_entries_copy_ctor(&intern->array, &other->array); + } else { + intern->array.circular_buffer = NULL; + } + + return &intern->std; +} + +static zend_object *collections_deque_new(zend_class_entry *class_type) +{ + return collections_deque_new_ex(class_type, NULL, 0); +} + +static zend_object *collections_deque_clone(zend_object *old_object) +{ + zend_object *new_object = collections_deque_new_ex(old_object->ce, old_object, 1); + + return new_object; +} + +static zend_result collections_deque_count_elements(zend_object *object, zend_long *count) +{ + const collections_deque *intern = collections_deque_from_object(object); + *count = intern->array.size; + return SUCCESS; +} + +/* Get the number of elements in this deque */ +PHP_METHOD(Collections_Deque, count) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + const collections_deque *intern = Z_DEQUE_P(ZEND_THIS); + RETURN_LONG(intern->array.size); +} + +/* Returns true if there are 0 elements in this Deque. */ +PHP_METHOD(Collections_Deque, isEmpty) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(!Z_DEQUE_ENTRIES_P(ZEND_THIS)->size); +} + +/* Get the capacity of this deque. Internal api meant for unit tests of Collections\Deque itself.. */ +PHP_METHOD(Collections_Deque, capacity) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_LONG(collections_deque_entries_get_capacity(Z_DEQUE_ENTRIES_P(ZEND_THIS))); +} + +/* Free elements and backing storage of this deque */ +PHP_METHOD(Collections_Deque, clear) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + collections_deque *intern = Z_DEQUE_P(ZEND_THIS); + if (intern->array.mask == 0) { + /* No backing storage to clear */ + return; + } + /* Immediately make the original storage inaccessible and set count/capacity to 0 in case destructors modify the deque */ + collections_deque_entries old_array = intern->array; + memset(&intern->array, 0, sizeof(intern->array)); + collections_deque_entries_dtor(&old_array); + if (intern->std.properties) { + zend_hash_clean(intern->std.properties); + } +} + +/* Create this from an iterable */ +PHP_METHOD(Collections_Deque, __construct) +{ + zval *object = ZEND_THIS; + zval* iterable = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ITERABLE(iterable) + ZEND_PARSE_PARAMETERS_END(); + + collections_deque *intern = Z_DEQUE_P(object); + + if (UNEXPECTED(!collections_deque_entries_uninitialized(&intern->array))) { + zend_throw_exception(spl_ce_RuntimeException, "Called Collections\\Deque::__construct twice", 0); + /* called __construct() twice, bail out */ + RETURN_THROWS(); + } + + if (iterable == NULL) { + intern->array.offset = 0; + intern->array.size = 0; + intern->array.mask = 0; + intern->array.circular_buffer = (zval *)collections_empty_entry_list; + return; + } + + switch (Z_TYPE_P(iterable)) { + case IS_ARRAY: + collections_deque_entries_init_from_array(&intern->array, Z_ARRVAL_P(iterable)); + return; + case IS_OBJECT: + collections_deque_entries_init_from_traversable(&intern->array, Z_OBJ_P(iterable)); + return; + EMPTY_SWITCH_DEFAULT_CASE(); + } +} + +PHP_METHOD(Collections_Deque, getIterator) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + zend_create_internal_iterator_zval(return_value, ZEND_THIS); +} + +static void collections_deque_it_dtor(zend_object_iterator *iter) +{ + collections_intrusive_dllist_node *node = &((collections_deque_it*)iter)->dllist_node; + collections_intrusive_dllist_remove(&Z_DEQUE_ENTRIES_P(&iter->data)->active_iterators, node); + zval_ptr_dtor(&iter->data); +} + +static void collections_deque_it_rewind(zend_object_iterator *iter) +{ + ((collections_deque_it*)iter)->current = 0; +} + +static int collections_deque_it_valid(zend_object_iterator *iter) +{ + const collections_deque_it *iterator = (collections_deque_it*)iter; + const collections_deque *object = Z_DEQUE_P(&iter->data); + return iterator->current < object->array.size ? SUCCESS : FAILURE; +} + +static zval *collections_deque_it_get_current_data(zend_object_iterator *iter) +{ + const collections_deque_it *iterator = (collections_deque_it*)iter; + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(&iter->data); + const uint32_t offset = iterator->current; + + if (UNEXPECTED(offset >= array->size)) { + collections_throw_invalid_sequence_index_exception(); + return &EG(uninitialized_zval); + } else { + return collections_deque_get_entry_at_offset(array, offset); + } +} + +static void collections_deque_it_get_current_key(zend_object_iterator *iter, zval *key) +{ + const collections_deque_it *iterator = (collections_deque_it*)iter; + const collections_deque_entries *array = Z_DEQUE_ENTRIES_P(&iter->data); + const uint32_t offset = iterator->current; + + if (offset >= array->size) { + ZVAL_NULL(key); + } else { + ZVAL_LONG(key, offset); + } +} + +static void collections_deque_it_move_forward(zend_object_iterator *iter) +{ + ((collections_deque_it*)iter)->current++; +} + +/* iterator handler table */ +static const zend_object_iterator_funcs collections_deque_it_funcs = { + collections_deque_it_dtor, + collections_deque_it_valid, + collections_deque_it_get_current_data, + collections_deque_it_get_current_key, + collections_deque_it_move_forward, + collections_deque_it_rewind, + NULL, + collections_internaliterator_get_gc, +}; + + +zend_object_iterator *collections_deque_get_iterator(zend_class_entry *ce, zval *object, int by_ref) +{ + if (UNEXPECTED(by_ref)) { + zend_throw_error(NULL, "An iterator cannot be used with foreach by reference"); + return NULL; + } + + collections_deque_it *iterator = emalloc(sizeof(collections_deque_it)); + + zend_iterator_init((zend_object_iterator*)iterator); + + zend_object *obj = Z_OBJ_P(object); + ZVAL_OBJ_COPY(&iterator->intern.data, obj); + iterator->intern.funcs = &collections_deque_it_funcs; + collections_intrusive_dllist_prepend(&collections_deque_entries_from_object(obj)->active_iterators, &iterator->dllist_node); + + (void) ce; + + return &iterator->intern; +} + +PHP_METHOD(Collections_Deque, __unserialize) +{ + HashTable *raw_data; + zval *val; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &raw_data) == FAILURE) { + RETURN_THROWS(); + } + + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + if (UNEXPECTED(!collections_deque_entries_uninitialized(array))) { + zend_throw_exception(spl_ce_RuntimeException, "Already unserialized", 0); + RETURN_THROWS(); + } + const uint32_t num_entries = zend_hash_num_elements(raw_data); + ZEND_ASSERT(array->circular_buffer == NULL); + + if (num_entries == 0) { + array->offset = 0; + array->size = 0; + array->mask = 0; + array->circular_buffer = (zval *)collections_empty_entry_list; + return; + } + + const uint32_t capacity = collections_deque_next_pow2_capacity(num_entries); + zval *const circular_buffer = safe_emalloc(capacity, sizeof(zval), 0); + zval *it = circular_buffer; + + zend_string *str; + + ZEND_HASH_FOREACH_STR_KEY_VAL(raw_data, str, val) { + if (UNEXPECTED(str)) { + for (zval *deleteIt = circular_buffer; deleteIt < it; deleteIt++) { + zval_ptr_dtor_nogc(deleteIt); + } + efree(circular_buffer); + zend_throw_exception(spl_ce_UnexpectedValueException, "Collections\\Deque::__unserialize saw unexpected string key, expected sequence of values", 0); + RETURN_THROWS(); + } + ZVAL_COPY_DEREF(it++, val); + } ZEND_HASH_FOREACH_END(); + ZEND_ASSERT(it <= circular_buffer + num_entries); + + array->size = it - circular_buffer; + array->mask = capacity - 1; + array->circular_buffer = circular_buffer; +} + +static void collections_deque_entries_init_from_array_values(collections_deque_entries *array, zend_array *raw_data) +{ + uint32_t num_entries = zend_hash_num_elements(raw_data); + if (num_entries == 0) { + array->size = 0; + array->mask = 0; + array->circular_buffer = NULL; + return; + } + const size_t capacity = collections_deque_next_pow2_capacity(num_entries); + ZEND_ASSERT(capacity >= num_entries); + zval * circular_buffer = safe_emalloc(capacity, sizeof(zval), 0); + uint32_t actual_size = 0; + zval *val; + ZEND_HASH_FOREACH_VAL(raw_data, val) { + ZVAL_COPY_DEREF(&circular_buffer[actual_size], val); + actual_size++; + } ZEND_HASH_FOREACH_END(); + + ZEND_ASSERT(actual_size <= num_entries); + + array->circular_buffer = circular_buffer; + array->size = actual_size; + array->mask = capacity - 1; + DEBUG_ASSERT_CONSISTENT_DEQUE(array); +} + +PHP_METHOD(Collections_Deque, __set_state) +{ + zend_array *array_ht; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(array_ht) + ZEND_PARSE_PARAMETERS_END(); + zend_object *object = collections_deque_new(collections_ce_Deque); + collections_deque *intern = collections_deque_from_object(object); + collections_deque_entries_init_from_array_values(&intern->array, array_ht); + + RETURN_OBJ(object); +} + +static zend_array* collections_deque_entries_to_refcounted_array(const collections_deque_entries *array) { + ZEND_ASSERT(array->mask > 0); + zval *const circular_buffer = array->circular_buffer; + zval *p = circular_buffer + array->offset; + zval *const end = circular_buffer + array->mask + 1; + uint32_t len = array->size; + zend_array *values = zend_new_array(len); + /* Initialize return array */ + zend_hash_real_init_packed(values); + + /* Go through values and add values to the return array */ + ZEND_HASH_FILL_PACKED(values) { + do { + Z_TRY_ADDREF_P(p); + ZEND_HASH_FILL_ADD(p); + p++; + if (p == end) { + p = circular_buffer; + } + len--; + } while (len > 0); + } ZEND_HASH_FILL_END(); + return values; +} + +PHP_METHOD(Collections_Deque, toArray) +{ + ZEND_PARSE_PARAMETERS_NONE(); + collections_deque *intern = Z_DEQUE_P(ZEND_THIS); + uint32_t len = intern->array.size; + if (!len) { + RETURN_EMPTY_ARRAY(); + } + RETURN_ARR(collections_deque_entries_to_refcounted_array(&intern->array)); +} + +static zend_always_inline void collections_deque_get_value_at_offset(zval *return_value, const zval *zval_this, zend_long offset) +{ + const collections_deque *intern = Z_DEQUE_P(zval_this); + uint32_t len = intern->array.size; + if (UNEXPECTED((zend_ulong) offset >= len)) { + collections_throw_invalid_sequence_index_exception(); + RETURN_THROWS(); + } + RETURN_COPY(collections_deque_get_entry_at_offset(&intern->array, offset)); +} + +PHP_METHOD(Collections_Deque, offsetGet) +{ + zval *offset_zv; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(offset_zv) + ZEND_PARSE_PARAMETERS_END(); + + /** + * NOTE: converting offset to long may have side effects such as emitting notices that mutate the deque. + * Do that before getting the state of the Deque. + */ + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); + + collections_deque_get_value_at_offset(return_value, ZEND_THIS, offset); +} + +PHP_METHOD(Collections_Deque, offsetExists) +{ + zval *offset_zv; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(offset_zv) + ZEND_PARSE_PARAMETERS_END(); + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); + + const collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + + if ((zend_ulong) offset >= array->size) { + RETURN_FALSE; + } + RETURN_BOOL(Z_TYPE_P(collections_deque_get_entry_at_offset(array, offset)) != IS_NULL); +} + +PHP_METHOD(Collections_Deque, containsKey) +{ + zval *offset_zv; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(offset_zv) + ZEND_PARSE_PARAMETERS_END(); + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); + + RETURN_LONG(((zend_ulong) offset) < Z_DEQUE_ENTRIES_P(ZEND_THIS)->size); +} + +static zval *collections_deque_read_dimension(zend_object *object, zval *offset_zv, int type, zval *rv) +{ + if (UNEXPECTED(!offset_zv)) { +handle_missing_key: + if (type != BP_VAR_IS) { + collections_throw_invalid_sequence_index_exception(); + return NULL; + } + return &EG(uninitialized_zval); + } + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW_RETURN_NULLPTR(offset, offset_zv); + + const collections_deque *intern = collections_deque_from_object(object); + + (void) rv; /* rv is not used */ + + if (UNEXPECTED(offset < 0 || (zend_ulong) offset >= intern->array.size)) { + goto handle_missing_key; + } else { + return collections_deque_get_entry_at_offset(&intern->array, offset); + } +} + +static zend_always_inline void collections_deque_entries_set_value_at_offset(collections_deque_entries *array, zend_long offset, zval *value) { + if (UNEXPECTED((zend_ulong) offset >= array->size)) { + collections_throw_invalid_sequence_index_exception(); + return; + } + zval *const ptr = collections_deque_get_entry_at_offset(array, offset); + zval tmp; + ZVAL_COPY_VALUE(&tmp, ptr); + ZVAL_COPY(ptr, value); + zval_ptr_dtor(&tmp); +} + +static zend_always_inline void collections_deque_entries_push_back(collections_deque_entries *array, zval *value) { + const uint32_t old_size = array->size; + const uint32_t old_mask = array->mask; + const size_t old_capacity = old_mask ? old_mask + 1 : 0; + + if (old_size >= old_capacity) { + ZEND_ASSERT(old_size == old_capacity); + collections_deque_entries_raise_capacity(array, old_capacity ? old_capacity * 2 : COLLECTIONS_DEQUE_MIN_CAPACITY); + } + array->size++; + zval *dest = collections_deque_get_entry_at_offset(array, old_size); + ZVAL_COPY(dest, value); +} + +static void collections_deque_write_dimension(zend_object *object, zval *offset_zv, zval *value) +{ + collections_deque_entries *array = collections_deque_entries_from_object(object); + if (!offset_zv) { + collections_deque_entries_push_back(array, value); + return; + } + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); + + if ((zend_ulong) offset >= array->size || offset < 0) { + zend_throw_exception(spl_ce_RuntimeException, "Index invalid or out of range", 0); + return; + } + ZVAL_DEREF(value); + collections_deque_entries_set_value_at_offset(array, offset, value); +} + +PHP_METHOD(Collections_Deque, offsetSet) +{ + zval *offset_zv, *value; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ZVAL(offset_zv) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); + + collections_deque_entries_set_value_at_offset(Z_DEQUE_ENTRIES_P(ZEND_THIS), offset, value); +} + +/* Copies all entries in the circular buffer in source starting at offset to *destination. The caller sets the new_capacity after calling this. */ +static void collections_deque_move_circular_buffer_to_new_buffer_of_capacity(collections_deque_entries *array, const size_t new_capacity) +{ + zval *const circular_buffer = array->circular_buffer; + const uint32_t size = array->size; + ZEND_ASSERT(array->mask > 0); + const size_t old_capacity = array->mask + 1; + const uint32_t first_len = old_capacity - array->offset; + ZEND_ASSERT(new_capacity >= size); + ZEND_ASSERT(old_capacity >= size); + zval *new_entries = safe_emalloc(new_capacity, sizeof(zval), 0); + /* There are 1 or 2 continuous segments of the circular buffer to copy to the start of the new circular buffer */ + if (size <= first_len) { + memcpy(new_entries, circular_buffer + array->offset, size * sizeof(zval)); + } else { + memcpy(new_entries, circular_buffer + array->offset, first_len * sizeof(zval)); + memcpy(new_entries + first_len, circular_buffer, (size - first_len) * sizeof(zval)); + } + efree(circular_buffer); + array->circular_buffer = new_entries; + array->offset = 0; +} + +static void collections_deque_entries_raise_capacity(collections_deque_entries *array, const size_t new_capacity) +{ + if (UNEXPECTED(new_capacity > COLLECTIONS_MAX_ZVAL_COLLECTION_SIZE)) { + /* This is a fatal error, because userland code might expect any catchable throwable + * from userland, not from the internal implementation. */ + zend_error_noreturn(E_ERROR, "Exceeded max valid Collections\\Deque capacity"); + ZEND_UNREACHABLE(); + } + const uint32_t old_mask = array->mask; + ZEND_ASSERT(new_capacity > 0 && collections_is_valid_uint32_capacity(new_capacity)); + ZEND_ASSERT(new_capacity > old_mask + 1); + if (collections_deque_entries_empty_capacity(array)) { + array->circular_buffer = safe_emalloc(new_capacity, sizeof(zval), 0); + } else if (array->offset + array->size <= old_mask + 1) { + array->circular_buffer = safe_erealloc(array->circular_buffer, new_capacity, sizeof(zval), 0); + } else { + collections_deque_move_circular_buffer_to_new_buffer_of_capacity(array, new_capacity); + } + array->mask = new_capacity - 1; + DEBUG_ASSERT_CONSISTENT_DEQUE(array); +} + +static void collections_deque_entries_shrink_capacity(collections_deque_entries *array, uint32_t new_capacity) +{ + ZEND_ASSERT(collections_is_valid_uint32_capacity(new_capacity)); + ZEND_ASSERT(array->mask >= COLLECTIONS_DEQUE_MIN_MASK); + ZEND_ASSERT(new_capacity < array->mask + 1); + /* Callers leave some spare capacity for future additions */ + ZEND_ASSERT(new_capacity > COLLECTIONS_DEQUE_MIN_MASK); + ZEND_ASSERT(!collections_deque_entries_empty_capacity(array)); + + if (array->offset + array->size < new_capacity) { + /* Shrink the array, probably without copying any data */ + array->circular_buffer = safe_erealloc(array->circular_buffer, new_capacity, sizeof(zval), 0); + } else { + collections_deque_move_circular_buffer_to_new_buffer_of_capacity(array, new_capacity); + } + array->mask = new_capacity - 1; +} + + +PHP_METHOD(Collections_Deque, push) +{ + const zval *args; + uint32_t argc; + + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('+', args, argc) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(argc == 0)) { + return; + } + + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + uint32_t old_size = array->size; + const size_t new_size = old_size + argc; + uint32_t mask = array->mask; + const uint32_t old_capacity = mask ? mask + 1 : 0; + + if (new_size > old_capacity) { + const uint32_t new_capacity = collections_deque_next_pow2_capacity(new_size); + collections_deque_entries_raise_capacity(array, new_capacity); + mask = array->mask; + } + zval *const circular_buffer = array->circular_buffer; + const uint32_t old_offset = array->offset; + + while (1) { + zval *dest = &circular_buffer[(old_offset + old_size) & mask]; + ZVAL_COPY(dest, args); + if (++old_size >= new_size) { + break; + } + args++; + } + array->size = new_size; +} + +static void collections_deque_adjust_iterators_before_remove(collections_deque_entries *array, collections_intrusive_dllist_node *node, const uint32_t removed_offset) { + const zend_object *const obj = collections_deque_to_object(array); + const uint32_t old_size = array->size; + ZEND_ASSERT(removed_offset < old_size); + do { + collections_deque_it *it = collections_deque_it_from_node(node); + if (Z_OBJ(it->intern.data) == obj) { + if (it->current >= removed_offset && it->current < old_size) { + it->current--; + } + } + ZEND_ASSERT(node != node->next); + node = node->next; + } while (node != NULL); +} + +static zend_always_inline void collections_deque_maybe_adjust_iterators_before_remove(collections_deque_entries *array, const uint32_t removed_offset) +{ + if (UNEXPECTED(array->active_iterators.first)) { + collections_deque_adjust_iterators_before_remove(array, array->active_iterators.first, removed_offset); + } +} + +static void collections_deque_adjust_iterators_before_insert(collections_deque_entries *const array, collections_intrusive_dllist_node *node, const uint32_t inserted_offset, uint32_t n) { + const zend_object *const obj = collections_deque_to_object(array); + const uint32_t old_size = array->size; + ZEND_ASSERT(inserted_offset <= old_size); + do { + collections_deque_it *it = collections_deque_it_from_node(node); + if (Z_OBJ(it->intern.data) == obj) { + if (it->current >= inserted_offset && it->current < old_size) { + it->current += n; + } + } + ZEND_ASSERT(node != node->next); + node = node->next; + } while (node != NULL); +} + +static zend_always_inline void collections_deque_maybe_adjust_iterators_before_insert(collections_deque_entries *const array, const uint32_t inserted_offset, const uint32_t n) +{ + ZEND_ASSERT(inserted_offset <= array->size); + if (UNEXPECTED(array->active_iterators.first)) { + collections_deque_adjust_iterators_before_insert(array, array->active_iterators.first, inserted_offset, n); + } +} + +PHP_METHOD(Collections_Deque, unshift) +{ + const zval *args; + uint32_t argc; + + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('+', args, argc) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(argc == 0)) { + return; + } + + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + collections_deque_maybe_adjust_iterators_before_insert(array, 0, argc); + + uint32_t old_size = array->size; + const size_t new_size = old_size + argc; + uint32_t mask = array->mask; + const uint32_t old_capacity = mask ? mask + 1 : 0; + + if (new_size > old_capacity) { + const size_t new_capacity = collections_deque_next_pow2_capacity(new_size); + collections_deque_entries_raise_capacity(array, new_capacity); + mask = array->mask; + } + uint32_t offset = array->offset; + zval *const circular_buffer = array->circular_buffer; + + do { + offset = (offset - 1) & mask; + zval *dest = &circular_buffer[offset]; + ZVAL_COPY(dest, args); + if (--argc == 0) { + break; + } + args++; + } while (1); + + array->offset = offset; + array->size = new_size; + + DEBUG_ASSERT_CONSISTENT_DEQUE(array); +} + +static zend_always_inline void collections_deque_entries_insert_values(collections_deque_entries *const array, const uint32_t inserted_offset, uint32_t argc, const zval *args) { + const uint32_t old_size = array->size; + const size_t new_size = old_size + argc; + uint32_t mask = array->mask; + const uint32_t old_capacity = mask ? mask + 1 : 0; + ZEND_ASSERT(argc > 0); + + if (new_size > old_capacity) { + const size_t new_capacity = collections_deque_next_pow2_capacity(new_size); + collections_deque_entries_raise_capacity(array, new_capacity); + mask = array->mask; + } + const uint32_t offset = array->offset; + zval *const circular_buffer = array->circular_buffer; + + collections_deque_maybe_adjust_iterators_before_insert(array, inserted_offset, argc); + + /* Move elements to the end of the deque */ + /* TODO move the start instead when there are less elements. */ + uint32_t src_offset = (offset + old_size) & mask; /* Masked in do-while loop. */ + uint32_t dst_offset = src_offset + argc; + const uint32_t src_end = (offset + inserted_offset) & mask; + + while (src_offset != src_end) { + src_offset = (src_offset - 1) & mask; + dst_offset = (dst_offset - 1) & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[src_offset]) != IS_UNDEF); + ZVAL_COPY_VALUE(&circular_buffer[dst_offset], &circular_buffer[src_offset]); + } + + dst_offset = (offset + inserted_offset) & mask; + do { + zval *dest = &circular_buffer[dst_offset]; + ZVAL_COPY(dest, args); + if (--argc == 0) { + break; + } + args++; + dst_offset = (dst_offset + 1) & mask; + } while (1); + + array->size = new_size; + + DEBUG_ASSERT_CONSISTENT_DEQUE(array); +} + +PHP_METHOD(Collections_Deque, insert) +{ + const zval *args; + zend_long inserted_offset; + uint32_t argc; + + ZEND_PARSE_PARAMETERS_START(1, -1) + Z_PARAM_LONG(inserted_offset) + Z_PARAM_VARIADIC('+', args, argc) + ZEND_PARSE_PARAMETERS_END(); + + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + if (UNEXPECTED(((zend_ulong) inserted_offset) > array->size)) { + collections_throw_invalid_sequence_index_exception(); + return; + } + ZEND_ASSERT(inserted_offset >= 0); + + if (UNEXPECTED(argc == 0)) { + return; + } + + collections_deque_entries_insert_values(array, inserted_offset, argc, args); +} + +static zend_always_inline void collections_deque_entries_remove_offset(collections_deque_entries *const array, const uint32_t removed_offset) { + const uint32_t old_size = array->size; + uint32_t mask = array->mask; + ZEND_ASSERT(removed_offset < old_size); + + const uint32_t offset = array->offset; + zval *const circular_buffer = array->circular_buffer; + + /* Move elements from the end of the deque to replace the removed element */ + /* TODO: Remove from the front instead if there are fewer elements to remove, adjust iterators */ + uint32_t it_offset = (offset + removed_offset) & mask; + + collections_deque_maybe_adjust_iterators_before_remove(array, removed_offset); + + zval removed_val; + ZVAL_COPY_VALUE(&removed_val, &circular_buffer[it_offset]); + const uint32_t it_end = (offset + old_size - 1) & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[it_offset]) != IS_UNDEF); + + while (it_offset != it_end) { + const uint32_t next_offset = (it_offset + 1) & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[next_offset]) != IS_UNDEF); + ZVAL_COPY_VALUE(&circular_buffer[it_offset], &circular_buffer[next_offset]); + it_offset = next_offset; + } + + const uint32_t new_size = old_size - 1; + array->size = new_size; + collections_deque_entries_try_shrink_capacity(array, new_size); + zval_ptr_dtor(&removed_val); + + DEBUG_ASSERT_CONSISTENT_DEQUE(array); +} + +PHP_METHOD(Collections_Deque, offsetUnset) +{ + zval *offset_zv; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(offset_zv) + ZEND_PARSE_PARAMETERS_END(); + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); + + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + const uint32_t old_size = array->size; + if (UNEXPECTED((zend_ulong) offset >= old_size)) { + COLLECTIONS_THROW_INVALID_SEQUENCE_INDEX_EXCEPTION(); + } + collections_deque_entries_remove_offset(array, offset); +} + +static zend_always_inline void collections_deque_entries_try_shrink_capacity(collections_deque_entries *array, uint32_t old_size) +{ + const uint32_t old_mask = array->mask; + if (old_size - 1 <= ((old_mask) >> 2) && old_mask > COLLECTIONS_DEQUE_MIN_MASK) { + collections_deque_entries_shrink_capacity(array, (old_mask >> 1) + 1); + } +} + +PHP_METHOD(Collections_Deque, pop) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + const uint32_t old_size = array->size; + if (old_size == 0) { + zend_throw_exception(spl_ce_UnderflowException, "Cannot pop from empty Collections\\Deque", 0); + RETURN_THROWS(); + } + + collections_deque_maybe_adjust_iterators_before_remove(array, old_size - 1); + + zval *val = collections_deque_get_entry_at_offset(array, old_size - 1); + + array->size--; + /* This is being removed. Use a macro that doesn't change the total reference count. */ + RETVAL_COPY_VALUE(val); + + ZEND_ASSERT(array->mask >= COLLECTIONS_DEQUE_MIN_MASK); + collections_deque_entries_try_shrink_capacity(array, old_size); +} + +PHP_METHOD(Collections_Deque, last) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + const collections_deque *intern = Z_DEQUE_P(ZEND_THIS); + const uint32_t old_size = intern->array.size; + if (old_size == 0) { + zend_throw_exception(spl_ce_UnderflowException, "Cannot read last value of empty Collections\\Deque", 0); + RETURN_THROWS(); + } + + /* This is being copied. Use a macro that increases the total reference count. */ + RETVAL_COPY(collections_deque_get_entry_at_offset(&intern->array, old_size - 1)); +} + +PHP_METHOD(Collections_Deque, shift) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + DEBUG_ASSERT_CONSISTENT_DEQUE(array); + const uint32_t old_size = array->size; + if (old_size == 0) { + zend_throw_exception(spl_ce_UnderflowException, "Cannot shift from empty Collections\\Deque", 0); + RETURN_THROWS(); + } + collections_deque_maybe_adjust_iterators_before_remove(array, 0); + + array->size--; + const uint32_t old_offset = array->offset; + const uint32_t old_mask = array->mask; + array->offset = (old_offset + 1) & old_mask; + RETVAL_COPY_VALUE(&array->circular_buffer[old_offset]); + collections_deque_entries_try_shrink_capacity(array, old_size); +} + +PHP_METHOD(Collections_Deque, first) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + const collections_deque_entries *array = Z_DEQUE_ENTRIES_P(ZEND_THIS); + DEBUG_ASSERT_CONSISTENT_DEQUE(array); + if (array->size == 0) { + zend_throw_exception(spl_ce_UnderflowException, "Cannot read first value of empty Collections\\Deque", 0); + RETURN_THROWS(); + } + + RETVAL_COPY(&array->circular_buffer[array->offset]); +} + +PHP_MINIT_FUNCTION(collections_deque) +{ + collections_ce_Deque = register_class_Collections_Deque(zend_ce_aggregate, zend_ce_countable, php_json_serializable_ce, zend_ce_arrayaccess); + collections_ce_Deque->create_object = collections_deque_new; + + memcpy(&collections_handler_Deque, &std_object_handlers, sizeof(zend_object_handlers)); + + collections_handler_Deque.offset = XtOffsetOf(collections_deque, std); + collections_handler_Deque.clone_obj = collections_deque_clone; + collections_handler_Deque.count_elements = collections_deque_count_elements; + /* Deliberately use default get_properties implementation for infinite recursion detection, creating empty table if one didn't exist. */ + collections_handler_Deque.get_properties_for = collections_deque_get_properties_for; + collections_handler_Deque.get_gc = collections_deque_get_gc; + collections_handler_Deque.dtor_obj = zend_objects_destroy_object; + collections_handler_Deque.free_obj = collections_deque_free_storage; + + collections_handler_Deque.read_dimension = collections_deque_read_dimension; + collections_handler_Deque.write_dimension = collections_deque_write_dimension; + //collections_handler_Deque.unset_dimension = collections_deque_unset_dimension; + //collections_handler_Deque.has_dimension = collections_deque_has_dimension; + + collections_ce_Deque->ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; + collections_ce_Deque->get_iterator = collections_deque_get_iterator; + + return SUCCESS; +} diff --git a/ext/collections/collections_deque.h b/ext/collections/collections_deque.h new file mode 100644 index 0000000000000..fe0f204495348 --- /dev/null +++ b/ext/collections/collections_deque.h @@ -0,0 +1,24 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Tyson Andre | + +----------------------------------------------------------------------+ +*/ + +#ifndef COLLECTIONS_DEQUE_H +#define COLLECTIONS_DEQUE_H + +extern zend_class_entry *collections_ce_Deque; + +PHP_MINIT_FUNCTION(collections_deque); + +#endif /* COLLECTIONS_DEQUE_H */ diff --git a/ext/collections/collections_deque.stub.php b/ext/collections/collections_deque.stub.php new file mode 100644 index 0000000000000..a8de7361b55e3 --- /dev/null +++ b/ext/collections/collections_deque.stub.php @@ -0,0 +1,112 @@ +count() + * AND the value at that offset is non-null. + */ + public function offsetExists(mixed $offset): bool {} + /** + * Sets the value at offset $offset (relative to the start of the Deque) to $value + * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this Deque. + */ + public function offsetSet(mixed $offset, mixed $value): void {} + /** + * Removes the value at (int)$offset from the deque. + * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this Deque. + */ + public function offsetUnset(mixed $offset): void {} + + /** + * This is JSON serialized as a JSON array with elements from the start to the end. + * @implementation-alias Collections\Deque::toArray + */ + public function jsonSerialize(): array {} +} diff --git a/ext/collections/collections_deque_arginfo.h b/ext/collections/collections_deque_arginfo.h new file mode 100644 index 0000000000000..9ba04d7e99496 --- /dev/null +++ b/ext/collections/collections_deque_arginfo.h @@ -0,0 +1,129 @@ +/* This is a generated file, edit the .stub.php file instead. + * Stub hash: 75aa473d9be94d2b02515dad1eed3b44dd850e47 */ + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Collections_Deque___construct, 0, 0, 0) + ZEND_ARG_OBJ_TYPE_MASK(0, iterator, Traversable, MAY_BE_ARRAY, "[]") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Collections_Deque_getIterator, 0, 0, InternalIterator, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_count, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_isEmpty, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_clear, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque___serialize, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque___unserialize, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Collections_Deque___set_state, 0, 1, Collections\\Deque, 0) + ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_push, 0, 0, IS_VOID, 0) + ZEND_ARG_VARIADIC_TYPE_INFO(0, values, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Collections_Deque_unshift arginfo_class_Collections_Deque_push + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_pop, 0, 0, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Collections_Deque_shift arginfo_class_Collections_Deque_pop + +#define arginfo_class_Collections_Deque_first arginfo_class_Collections_Deque_pop + +#define arginfo_class_Collections_Deque_last arginfo_class_Collections_Deque_pop + +#define arginfo_class_Collections_Deque_toArray arginfo_class_Collections_Deque___serialize + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_insert, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) + ZEND_ARG_VARIADIC_TYPE_INFO(0, values, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_offsetGet, 0, 1, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_offsetExists, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_offsetSet, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_offsetUnset, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Collections_Deque_jsonSerialize arginfo_class_Collections_Deque___serialize + + +ZEND_METHOD(Collections_Deque, __construct); +ZEND_METHOD(Collections_Deque, getIterator); +ZEND_METHOD(Collections_Deque, count); +ZEND_METHOD(Collections_Deque, isEmpty); +ZEND_METHOD(Collections_Deque, clear); +ZEND_METHOD(Collections_Deque, toArray); +ZEND_METHOD(Collections_Deque, __unserialize); +ZEND_METHOD(Collections_Deque, __set_state); +ZEND_METHOD(Collections_Deque, push); +ZEND_METHOD(Collections_Deque, unshift); +ZEND_METHOD(Collections_Deque, pop); +ZEND_METHOD(Collections_Deque, shift); +ZEND_METHOD(Collections_Deque, first); +ZEND_METHOD(Collections_Deque, last); +ZEND_METHOD(Collections_Deque, insert); +ZEND_METHOD(Collections_Deque, offsetGet); +ZEND_METHOD(Collections_Deque, offsetExists); +ZEND_METHOD(Collections_Deque, offsetSet); +ZEND_METHOD(Collections_Deque, offsetUnset); + + +static const zend_function_entry class_Collections_Deque_methods[] = { + ZEND_ME(Collections_Deque, __construct, arginfo_class_Collections_Deque___construct, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, getIterator, arginfo_class_Collections_Deque_getIterator, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, count, arginfo_class_Collections_Deque_count, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, isEmpty, arginfo_class_Collections_Deque_isEmpty, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, clear, arginfo_class_Collections_Deque_clear, ZEND_ACC_PUBLIC) + ZEND_MALIAS(Collections_Deque, __serialize, toArray, arginfo_class_Collections_Deque___serialize, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, __unserialize, arginfo_class_Collections_Deque___unserialize, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, __set_state, arginfo_class_Collections_Deque___set_state, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(Collections_Deque, push, arginfo_class_Collections_Deque_push, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, unshift, arginfo_class_Collections_Deque_unshift, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, pop, arginfo_class_Collections_Deque_pop, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, shift, arginfo_class_Collections_Deque_shift, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, first, arginfo_class_Collections_Deque_first, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, last, arginfo_class_Collections_Deque_last, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, toArray, arginfo_class_Collections_Deque_toArray, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, insert, arginfo_class_Collections_Deque_insert, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, offsetGet, arginfo_class_Collections_Deque_offsetGet, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, offsetExists, arginfo_class_Collections_Deque_offsetExists, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, offsetSet, arginfo_class_Collections_Deque_offsetSet, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, offsetUnset, arginfo_class_Collections_Deque_offsetUnset, ZEND_ACC_PUBLIC) + ZEND_MALIAS(Collections_Deque, jsonSerialize, toArray, arginfo_class_Collections_Deque_jsonSerialize, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static zend_class_entry *register_class_Collections_Deque(zend_class_entry *class_entry_IteratorAggregate, zend_class_entry *class_entry_Countable, zend_class_entry *class_entry_JsonSerializable, zend_class_entry *class_entry_ArrayAccess) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Collections", "Deque", class_Collections_Deque_methods); + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_FINAL; + zend_class_implements(class_entry, 4, class_entry_IteratorAggregate, class_entry_Countable, class_entry_JsonSerializable, class_entry_ArrayAccess); + + return class_entry; +} diff --git a/ext/collections/collections_internaliterator.h b/ext/collections/collections_internaliterator.h new file mode 100644 index 0000000000000..687d8554f0974 --- /dev/null +++ b/ext/collections/collections_internaliterator.h @@ -0,0 +1,58 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Tyson Andre | + +----------------------------------------------------------------------+ +*/ + +#ifndef COLLECTIONS_INTERNALITERATOR_H +#define COLLECTIONS_INTERNALITERATOR_H + +typedef struct _collections_intrusive_dllist_node { + struct _collections_intrusive_dllist_node *prev; + struct _collections_intrusive_dllist_node *next; +} collections_intrusive_dllist_node; + +typedef struct _collections_intrusive_dllist { + struct _collections_intrusive_dllist_node *first; +} collections_intrusive_dllist; + +static zend_always_inline void collections_intrusive_dllist_prepend(collections_intrusive_dllist *list, collections_intrusive_dllist_node *node) { + collections_intrusive_dllist_node *first = list->first; + ZEND_ASSERT(node != first); + node->next = first; + node->prev = NULL; + list->first = node; + + if (first) { + ZEND_ASSERT(first->prev == NULL); + first->prev = node; + } +} + +static zend_always_inline void collections_intrusive_dllist_remove(collections_intrusive_dllist *list, const collections_intrusive_dllist_node *node) { + collections_intrusive_dllist_node *next = node->next; + collections_intrusive_dllist_node *prev = node->prev; + ZEND_ASSERT(node != next); + ZEND_ASSERT(node != prev); + ZEND_ASSERT(next != prev || next == NULL); + if (next) { + next->prev = prev; + } + if (list->first == node) { + list->first = next; + ZEND_ASSERT(prev == NULL); + } else if (prev) { + prev->next = next; + } +} +#endif diff --git a/ext/collections/collections_util.c b/ext/collections/collections_util.c new file mode 100644 index 0000000000000..19b13ac77ebff --- /dev/null +++ b/ext/collections/collections_util.c @@ -0,0 +1,41 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Tyson Andre | + +----------------------------------------------------------------------+ +*/ + +#include "collections_util.h" + +/* Override get_properties_for and use the default implementation of get_properties. See https://github.com/php/php-src/issues/9697#issuecomment-1273613175 */ +HashTable* collections_noop_empty_array_get_properties_for(zend_object *obj, zend_prop_purpose purpose) { + (void)obj; + (void)purpose; + return NULL; +} + +HashTable* collections_noop_get_gc(zend_object *obj, zval **table, int *n) { + /* Zend/zend_gc.c does not initialize table or n. So we need to set n to 0 at minimum. */ + *n = 0; + (void) table; + (void) obj; + /* Nothing needs to be garbage collected */ + return NULL; +} + +HashTable *collections_internaliterator_get_gc(zend_object_iterator *iter, zval **table, int *n) +{ + *table = &iter->data; + *n = 1; + return NULL; +} + diff --git a/ext/collections/collections_util.h b/ext/collections/collections_util.h new file mode 100644 index 0000000000000..5fcd762102259 --- /dev/null +++ b/ext/collections/collections_util.h @@ -0,0 +1,97 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Tyson Andre | + +----------------------------------------------------------------------+ +*/ +#ifndef COLLECTIONS_UTIL_H +#define COLLECTIONS_UTIL_H + +#include "Zend/zend.h" + +#define COLLECTIONS_MAX_ZVAL_COLLECTION_SIZE HT_MAX_SIZE +static zend_always_inline uint32_t collections_next_pow2_capacity_uint32(uint32_t nSize, uint32_t min) { + if (nSize < min) { + return min; + } + /* Note that for values such as 63 or 31 of the form ((2^n) - 1), + * subtracting and xor are the same things for numbers in the range of 0 to the max. */ +#ifdef ZEND_WIN32 + unsigned long index; + if (BitScanReverse(&index, nSize - 1)) { + return 0x2u << ((31 - index) ^ 0x1f); + } + /* nSize is ensured to be in the valid range, fall back to it + * rather than using an undefined bit scan result. */ + return nSize; +#elif (defined(__GNUC__) || __has_builtin(__builtin_clz)) && defined(PHP_HAVE_BUILTIN_CLZ) + return 0x2u << (__builtin_clz(nSize - 1) ^ 0x1f); +#endif + nSize -= 1; + nSize |= (nSize >> 1); + nSize |= (nSize >> 2); + nSize |= (nSize >> 4); + nSize |= (nSize >> 8); + nSize |= (nSize >> 16); + return nSize + 1; +} + +static zend_always_inline size_t collections_next_pow2_capacity(size_t nSize, size_t min) { +#if SIZEOF_SIZE_T <= 4 + return collections_next_pow2_capacity_uint32(nSize, min); +#else + if (nSize < min) { + return min; + } + /* Note that for values such as 63 or 31 of the form ((2^n) - 1), + * subtracting and xor are the same things for numbers in the range of 0 to the max. */ +#ifdef ZEND_WIN32 + unsigned long index; + if (BitScanReverse64(&index, nSize - 1)) { + return 0x2u << ((63 - index) ^ 0x3f); + } + /* nSize is ensured to be in the valid range, fall back to it + * rather than using an undefined bit scan result. */ + return nSize; +#elif (defined(__GNUC__) || __has_builtin(__builtin_clz)) && defined(PHP_HAVE_BUILTIN_CLZ) +#if SIZEOF_SIZE_T > SIZEOF_INT + return 0x2u << (__builtin_clzl(nSize - 1) ^ (sizeof(long) * 8 - 1)); +#else + return 0x2u << (__builtin_clz(nSize - 1) ^ 0x1f); +#endif +#else + nSize -= 1; + nSize |= (nSize >> 1); + nSize |= (nSize >> 2); + nSize |= (nSize >> 4); + nSize |= (nSize >> 8); + nSize |= (nSize >> 16); + nSize |= (nSize >> 32); + return nSize + 1; +#endif +#endif +} + +/** + * Returns absence of zvals or hash table to garbage collect. + * (e.g. for collections that are immutable or made of scalars instead of zvals) + */ +HashTable* collections_noop_get_gc(zend_object *obj, zval **table, int *n); +/** + * Returns the immutable empty array in a get_properties handler. + * This is useful to keep memory low when a datastructure is guaranteed to be free of cycles (e.g. only scalars, or empty) + */ +HashTable* collections_noop_empty_array_get_properties_for(zend_object *obj, zend_prop_purpose purpose); + +HashTable *collections_internaliterator_get_gc(zend_object_iterator *iter, zval **table, int *n); + +#endif diff --git a/ext/collections/config.m4 b/ext/collections/config.m4 new file mode 100644 index 0000000000000..768e180c385ef --- /dev/null +++ b/ext/collections/config.m4 @@ -0,0 +1,5 @@ +PHP_NEW_EXTENSION(collections, php_collections.c collections_deque.c collections_util.c, no,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) +PHP_INSTALL_HEADERS([ext/collections], [php_collections.h collections_deque.h collections_internaliterator.h collections_util.h]) +PHP_ADD_EXTENSION_DEP(collections, spl, true) +PHP_ADD_EXTENSION_DEP(collections, standard, true) +PHP_ADD_EXTENSION_DEP(collections, json) diff --git a/ext/collections/config.w32 b/ext/collections/config.w32 new file mode 100644 index 0000000000000..218dbe1771b18 --- /dev/null +++ b/ext/collections/config.w32 @@ -0,0 +1,5 @@ +// vim:ft=javascript + +EXTENSION("collections", "php_collections.c collections_deque.c collections_util.c", false /*never shared */, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); +PHP_COLLECTIONS="yes"; +PHP_INSTALL_HEADERS("ext/collections", "php_collections.h collections_deque.h collections_internaliterator.h collections_util.h"); diff --git a/ext/collections/php_collections.c b/ext/collections/php_collections.c new file mode 100644 index 0000000000000..69816c1a84b85 --- /dev/null +++ b/ext/collections/php_collections.c @@ -0,0 +1,64 @@ +/* + +----------------------------------------------------------------------+ + | collections extension for PHP | + | See COPYING file for further copyright information | + +----------------------------------------------------------------------+ + | Author: Tyson Andre | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "php.h" + +#include "ext/standard/info.h" + +#include "collections_deque.h" + +#include "php_collections.h" + +/* {{{ PHP_MINIT_FUNCTION */ +PHP_MINIT_FUNCTION(collections) +{ + PHP_MINIT(collections_deque)(INIT_FUNC_ARGS_PASSTHRU); + return SUCCESS; +} +/* }}} */ + +/* {{{ PHP_MINFO_FUNCTION */ +PHP_MINFO_FUNCTION(collections) +{ + (void) ((ZEND_MODULE_INFO_FUNC_ARGS_PASSTHRU)); + php_info_print_table_start(); + php_info_print_table_header(2, "collections support", "enabled"); + php_info_print_table_end(); +} +/* }}} */ + +/* {{{ collections_module_entry */ +static const zend_module_dep collections_deps[] = { + ZEND_MOD_REQUIRED("spl") + ZEND_MOD_REQUIRED("json") + ZEND_MOD_END +}; + +zend_module_entry collections_module_entry = { + STANDARD_MODULE_HEADER_EX, NULL, + collections_deps, + "collections", /* Extension name */ + NULL, /* zend_function_entry */ + PHP_MINIT(collections), /* PHP_MINIT - Module initialization */ + NULL, /* PHP_MSHUTDOWN - Module shutdown */ + NULL, /* PHP_RINIT - Request initialization */ + NULL, /* PHP_RSHUTDOWN - Request shutdown */ + PHP_MINFO(collections), /* PHP_MINFO - Module info */ + PHP_COLLECTIONS_VERSION, /* Version */ + STANDARD_MODULE_PROPERTIES +}; +/* }}} */ + +#ifdef COMPILE_DL_COLLECTIONS +ZEND_GET_MODULE(collections) +#endif diff --git a/ext/collections/php_collections.h b/ext/collections/php_collections.h new file mode 100644 index 0000000000000..9f7d83dbba7f5 --- /dev/null +++ b/ext/collections/php_collections.h @@ -0,0 +1,31 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Tyson Andre | + +----------------------------------------------------------------------+ + */ + +#ifndef PHP_COLLECTIONS_H +#define PHP_COLLECTIONS_H + +#include "php.h" +#include + +#define PHP_COLLECTIONS_VERSION PHP_VERSION + +extern zend_module_entry collections_module_entry; +#define phpext_collections_ptr &collections_module_entry + +PHP_MINIT_FUNCTION(collections); +PHP_MINFO_FUNCTION(collections); + +#endif /* PHP_COLLECTIONS_H */ diff --git a/ext/collections/tests/Deque/Deque.phpt b/ext/collections/tests/Deque/Deque.phpt new file mode 100644 index 0000000000000..592fead1bbfa9 --- /dev/null +++ b/ext/collections/tests/Deque/Deque.phpt @@ -0,0 +1,44 @@ +--TEST-- +Collections\Deque constructed from array +--FILE-- + 'x', 'second' => new stdClass()]); +foreach ($it as $key => $value) { + printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); +} +var_dump($it); +var_dump((array)$it); + +$it = new Collections\Deque([]); +var_dump($it); +var_dump((array)$it); +foreach ($it as $key => $value) { + echo "Unreachable\n"; +} + +?> +--EXPECT-- +Key: 0 +Value: 'x' +Key: 1 +Value: (object) array( +) +object(Collections\Deque)#1 (2) { + [0]=> + string(1) "x" + [1]=> + object(stdClass)#2 (0) { + } +} +array(2) { + [0]=> + string(1) "x" + [1]=> + object(stdClass)#2 (0) { + } +} +object(Collections\Deque)#3 (0) { +} +array(0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/aggregate.phpt b/ext/collections/tests/Deque/aggregate.phpt new file mode 100644 index 0000000000000..4609301ca0e4b --- /dev/null +++ b/ext/collections/tests/Deque/aggregate.phpt @@ -0,0 +1,17 @@ +--TEST-- +Collections\Deque is an IteratorAggregate +--FILE-- + 'x', 'discardedsecond' => (object)['key' => 'value']]); +foreach ($it as $k1 => $v1) { + foreach ($it as $k2 => $v2) { + printf("k1=%s k2=%s v1=%s v2=%s\n", json_encode($k1), json_encode($k2), json_encode($v1), json_encode($v2)); + } +} +?> +--EXPECT-- +k1=0 k2=0 v1="x" v2="x" +k1=0 k2=1 v1="x" v2={"key":"value"} +k1=1 k2=0 v1={"key":"value"} v2="x" +k1=1 k2=1 v1={"key":"value"} v2={"key":"value"} \ No newline at end of file diff --git a/ext/collections/tests/Deque/arrayCast.phpt b/ext/collections/tests/Deque/arrayCast.phpt new file mode 100644 index 0000000000000..fa8ef8626b747 --- /dev/null +++ b/ext/collections/tests/Deque/arrayCast.phpt @@ -0,0 +1,30 @@ +--TEST-- +Collections\Deque to array +--FILE-- +pop(); +var_dump($it); + + +?> +--EXPECT-- +array(1) { + [0]=> + string(4) "TEST" +} +array(1) { + [0]=> + string(5) "TEST2" +} +object(Collections\Deque)#1 (1) { + [0]=> + string(5) "TEST2" +} +object(Collections\Deque)#1 (0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/clear.phpt b/ext/collections/tests/Deque/clear.phpt new file mode 100644 index 0000000000000..ea6ef39fa1274 --- /dev/null +++ b/ext/collections/tests/Deque/clear.phpt @@ -0,0 +1,25 @@ +--TEST-- +Collections\Deque clear +--FILE-- + new stdClass()]); +var_dump($it->toArray()); +printf("count=%d\n", $it->count()); +$it->clear(); +foreach ($it as $value) { + echo "Not reached\n"; +} +var_dump($it->toArray()); +printf("count=%d\n", $it->count()); +?> +--EXPECT-- +array(1) { + [0]=> + object(stdClass)#2 (0) { + } +} +count=1 +array(0) { +} +count=0 \ No newline at end of file diff --git a/ext/collections/tests/Deque/clone.phpt b/ext/collections/tests/Deque/clone.phpt new file mode 100644 index 0000000000000..952e918a3e26a --- /dev/null +++ b/ext/collections/tests/Deque/clone.phpt @@ -0,0 +1,20 @@ +--TEST-- +Collections\Deque can be cloned after building properties table. +--FILE-- + +--EXPECT-- +object(Collections\Deque)#1 (1) { + [0]=> + object(stdClass)#2 (0) { + } +} +object(Collections\Deque)#3 (1) { + [0]=> + object(stdClass)#2 (0) { + } +} diff --git a/ext/collections/tests/Deque/exceptionhandler.phpt b/ext/collections/tests/Deque/exceptionhandler.phpt new file mode 100644 index 0000000000000..484e4e094cecf --- /dev/null +++ b/ext/collections/tests/Deque/exceptionhandler.phpt @@ -0,0 +1,36 @@ +--TEST-- +Collections\Deque constructed from Traversable throwing +--FILE-- +value\n"; + } +} + +function yields_and_throws() { + yield 123 => new HasDestructor('in value'); + yield new HasDestructor('in key') => 123; + yield 123 => new HasDestructor('in value'); + yield 'first' => 'second'; + + throw new RuntimeException('test'); + + echo "Unreachable\n"; +} +try { + $it = new Collections\Deque(yields_and_throws()); +} catch (RuntimeException $e) { + echo "Caught " . $e->getMessage() . "\n"; +} +gc_collect_cycles(); +echo "Done\n"; +?> +--EXPECT-- +in HasDestructor::__destruct in key +in HasDestructor::__destruct in value +in HasDestructor::__destruct in value +Caught test +Done diff --git a/ext/collections/tests/Deque/foreach.phpt b/ext/collections/tests/Deque/foreach.phpt new file mode 100644 index 0000000000000..97055ef741a08 --- /dev/null +++ b/ext/collections/tests/Deque/foreach.phpt @@ -0,0 +1,98 @@ +--TEST-- +Collections\Deque modification during foreach +--FILE-- + $value) { + if (strlen($value) === 1) { + $deque->push("{$value}_"); + $deque->unshift("_$value"); + } + printf("Key: %s Value: %s\n", var_export($key, true), var_export($value, true)); +} +var_dump($deque); +echo "Test shift\n"; +foreach ($deque as $key => $value) { + echo "Shifting $key $value\n"; + var_dump($deque->shift()); +} + +echo "Test shift out of bounds\n"; +$deque = new Collections\Deque([mut('a1'), mut('b1'), mut('c1'), mut('d1')]); +foreach ($deque as $key => $value) { + var_dump($deque->shift()); + var_dump($deque->shift()); + echo "Saw $key: $value\n"; + // iteration does not stop early, iterator points to just before start of Deque +} +var_dump($deque); + +echo "Test iteration behavior\n"; +$deque = new Collections\Deque([mut('a1'), mut('a2')]); +$it = $deque->getIterator(); +echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n"; +$deque->shift(); +// invalid, outside the range of the deque +echo json_encode(['valid' => $it->valid(), 'key' => $it->key()]), "\n"; +$it->next(); +echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n"; +$deque->unshift('a', 'b'); +unset($deque); +echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n"; + +?> +--EXPECT-- +Test push/unshift +Key: 0 Value: 'a' +Key: 2 Value: 'b' +Key: 4 Value: 'a_' +Key: 5 Value: 'b_' +object(Collections\Deque)#1 (6) { + [0]=> + string(2) "_b" + [1]=> + string(2) "_a" + [2]=> + string(1) "a" + [3]=> + string(1) "b" + [4]=> + string(2) "a_" + [5]=> + string(2) "b_" +} +Test shift +Shifting 0 _b +string(2) "_b" +Shifting 0 _a +string(2) "_a" +Shifting 0 a +string(1) "a" +Shifting 0 b +string(1) "b" +Shifting 0 a_ +string(2) "a_" +Shifting 0 b_ +string(2) "b_" +Test shift out of bounds +string(2) "a1" +string(2) "b1" +Saw 0: a1 +string(2) "c1" +string(2) "d1" +Saw 0: c1 +object(Collections\Deque)#2 (0) { +} +Test iteration behavior +{"valid":true,"key":0,"value":"a1"} +{"valid":false,"key":null} +{"valid":true,"key":0,"value":"a2"} +{"valid":true,"key":2,"value":"a2"} diff --git a/ext/collections/tests/Deque/isEmpty.phpt b/ext/collections/tests/Deque/isEmpty.phpt new file mode 100644 index 0000000000000..09b095e4af9fa --- /dev/null +++ b/ext/collections/tests/Deque/isEmpty.phpt @@ -0,0 +1,15 @@ +--TEST-- +Collections\Deque isEmpty() +--FILE-- +isEmpty()); +$it->push(123); +var_dump($it->isEmpty()); +$it->push('other'); +var_dump($it->isEmpty()); +?> +--EXPECT-- +bool(true) +bool(false) +bool(false) diff --git a/ext/collections/tests/Deque/iterator.phpt b/ext/collections/tests/Deque/iterator.phpt new file mode 100644 index 0000000000000..321d227dad215 --- /dev/null +++ b/ext/collections/tests/Deque/iterator.phpt @@ -0,0 +1,50 @@ +--TEST-- +Collections\Deque iterator +--FILE-- +getMessage()); + } +} + +// 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. +$dq = new Collections\Deque([new stdClass(), strtoupper('test')]); +$it = $dq->getIterator(); +var_dump($it->key()); +var_dump($it->current()); +var_dump($it->next()); +var_dump($it->valid()); +echo "After shift\n"; +$dq->shift(); +var_dump($it->key()); +var_dump($it->current()); +var_dump($it->valid()); +$dq->shift(); +var_dump($it->key()); // null for invalid iterator +expect_throws(fn() => $it->current()); +var_dump($it->valid()); +foreach ($it as $key => $value) { + printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); +} +var_dump($it); +?> +--EXPECT-- +int(0) +object(stdClass)#2 (0) { +} +NULL +bool(true) +After shift +int(0) +string(4) "TEST" +bool(true) +NULL +Caught OutOfBoundsException: Index out of range +bool(false) +object(InternalIterator)#4 (0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/offsetGet.phpt b/ext/collections/tests/Deque/offsetGet.phpt new file mode 100644 index 0000000000000..58b4f7d63d5cd --- /dev/null +++ b/ext/collections/tests/Deque/offsetGet.phpt @@ -0,0 +1,67 @@ +--TEST-- +Collections\Deque offsetGet/get +--FILE-- +getMessage()); + } +} +expect_throws(fn() => (new ReflectionClass(Collections\Deque::class))->newInstanceWithoutConstructor()); +$it = new Collections\Deque(['first' => new stdClass()]); +var_dump($it->offsetGet(0)); +expect_throws(fn() => $it->offsetSet(1,'x')); +expect_throws(fn() => $it->offsetUnset(PHP_INT_MIN)); +expect_throws(fn() => $it->offsetUnset(-1)); +expect_throws(fn() => $it->offsetUnset(count($it))); +var_dump($it->offsetGet('0')); +echo "offsetExists checks\n"; +var_dump($it->offsetExists(1)); +var_dump($it->offsetExists('1')); +var_dump($it->offsetExists(PHP_INT_MAX)); +var_dump($it->offsetExists(PHP_INT_MIN)); +expect_throws(fn() => $it->offsetGet(1)); +expect_throws(fn() => $it->offsetGet(-1)); +echo "Invalid offsetGet calls\n"; +expect_throws(fn() => $it->offsetGet(PHP_INT_MAX)); +expect_throws(fn() => $it->offsetGet(PHP_INT_MIN)); +expect_throws(fn() => $it->offsetGet('1')); +expect_throws(fn() => $it->offsetGet('invalid')); +expect_throws(fn() => $it[['invalid']]); +expect_throws(fn() => $it->offsetUnset(PHP_INT_MAX)); +expect_throws(fn() => $it->offsetSet(PHP_INT_MAX,'x')); +expect_throws(function () use ($it) { unset($it[-1]); }); +var_dump($it->getIterator()); +?> +--EXPECT-- +Caught ReflectionException: Class Collections\Deque is an internal class marked as final that cannot be instantiated without invoking its constructor +object(stdClass)#1 (0) { +} +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +object(stdClass)#1 (0) { +} +offsetExists checks +bool(false) +bool(false) +bool(false) +bool(false) +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Invalid offsetGet calls +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Caught TypeError: Illegal offset type +Caught TypeError: Illegal offset type +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +object(InternalIterator)#4 (0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/offsetGetShifted.phpt b/ext/collections/tests/Deque/offsetGetShifted.phpt new file mode 100644 index 0000000000000..e491ecc2be3c0 --- /dev/null +++ b/ext/collections/tests/Deque/offsetGetShifted.phpt @@ -0,0 +1,54 @@ +--TEST-- +Collections\Deque offsetGet after push/pop +--FILE-- +getMessage()); + } +} +$it = new Collections\Deque(); +for ($i = 0; $i < 7; $i++) { + $it->push("x$i"); +} +var_dump($it->shift()); +$it->push('new'); +$it->push('another'); +for ($i = 0; $i < count($it); $i++) { + $it[$i] = $it[$i] . "_$i"; + var_dump($it[$i]); +} +foreach ($it as $i => $value) { + printf("foreach key=%d: %s\n", $i, $value); +} +echo json_encode($it), "\n"; +printf("count=%d\n", count($it)); + +expect_throws(fn() => $it[-1]); +expect_throws(fn() => $it[8]); +--EXPECT-- +string(2) "x0" +string(4) "x1_0" +string(4) "x2_1" +string(4) "x3_2" +string(4) "x4_3" +string(4) "x5_4" +string(4) "x6_5" +string(5) "new_6" +string(9) "another_7" +foreach key=0: x1_0 +foreach key=1: x2_1 +foreach key=2: x3_2 +foreach key=3: x4_3 +foreach key=4: x5_4 +foreach key=5: x6_5 +foreach key=6: new_6 +foreach key=7: another_7 +["x1_0","x2_1","x3_2","x4_3","x5_4","x6_5","new_6","another_7"] +count=8 +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range diff --git a/ext/collections/tests/Deque/offsetSet.phpt b/ext/collections/tests/Deque/offsetSet.phpt new file mode 100644 index 0000000000000..e14478a713bfd --- /dev/null +++ b/ext/collections/tests/Deque/offsetSet.phpt @@ -0,0 +1,37 @@ +--TEST-- +Collections\Deque offsetSet/set +--FILE-- +getMessage()); + } +} + +echo "Test empty deque\n"; +$it = new Collections\Deque([]); +expect_throws(fn() => $it->offsetSet(0, strtoupper('value'))); + +echo "Test short deque\n"; +$str = 'Test short deque'; +$it = new Collections\Deque(explode(' ', $str)); +$it->offsetSet(0, 'new'); +$it->offsetSet(2, strtoupper('test')); +echo json_encode($it), "\n"; +expect_throws(fn() => $it->offsetSet(-1, strtoupper('value'))); +expect_throws(fn() => $it->offsetSet(3, 'end')); +expect_throws(fn() => $it->offsetSet(PHP_INT_MAX, 'end')); + +?> +--EXPECT-- +Test empty deque +Caught OutOfBoundsException: Index out of range +Test short deque +["new","short","TEST"] +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range \ No newline at end of file diff --git a/ext/collections/tests/Deque/popFront.phpt b/ext/collections/tests/Deque/popFront.phpt new file mode 100644 index 0000000000000..b4eddb562f5a8 --- /dev/null +++ b/ext/collections/tests/Deque/popFront.phpt @@ -0,0 +1,13 @@ +--TEST-- +Collections\Deque shift +--FILE-- +shift()); +var_dump($dq[0]); +?> +--EXPECT-- +object(stdClass)#2 (0) { +} +string(4) "TEST" \ No newline at end of file diff --git a/ext/collections/tests/Deque/popMany.phpt b/ext/collections/tests/Deque/popMany.phpt new file mode 100644 index 0000000000000..ea6f43102669e --- /dev/null +++ b/ext/collections/tests/Deque/popMany.phpt @@ -0,0 +1,40 @@ +--TEST-- +Collections\Deque push repeatedly +--FILE-- +push("v$i"); +} +$dq->shift(); +$dq->push("extra"); +printf("dq=%s count=%d\n", json_encode($dq), $dq->count()); +for ($i = 0; $i < 7; $i++) { + $value = $dq->shift(); + printf("popped %s\n", $value); +} +// Collections\Deque should reclaim memory once roughly a quarter of the memory is actually used. +printf("dq=%s count=%d\n", json_encode($dq), $dq->count()); +var_dump($dq); +$dq->clear(); +printf("dq=%s count=%d\n", json_encode($dq), $dq->count()); +var_dump($dq); +?> +--EXPECT-- +dq=["v1","v2","v3","v4","v5","v6","v7","extra"] count=8 +popped v1 +popped v2 +popped v3 +popped v4 +popped v5 +popped v6 +popped v7 +dq=["extra"] count=1 +object(Collections\Deque)#1 (1) { + [0]=> + string(5) "extra" +} +dq=[] count=0 +object(Collections\Deque)#1 (0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/pushFront.phpt b/ext/collections/tests/Deque/pushFront.phpt new file mode 100644 index 0000000000000..b68fe9ec2c858 --- /dev/null +++ b/ext/collections/tests/Deque/pushFront.phpt @@ -0,0 +1,22 @@ +--TEST-- +Collections\Deque unshift +--FILE-- +unshift("$i"); +} +foreach ($it as $value) { + echo "$value,"; +} +echo "\n"; +$values = []; +while (count($it) > 0) { + $values[] = $it->shift(); +} +echo json_encode($values), "\n"; +?> +--EXPECT-- +19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0, +["19","18","17","16","15","14","13","12","11","10","9","8","7","6","5","4","3","2","1","0"] \ No newline at end of file diff --git a/ext/collections/tests/Deque/push_multiple.phpt b/ext/collections/tests/Deque/push_multiple.phpt new file mode 100644 index 0000000000000..33c18585fb9d6 --- /dev/null +++ b/ext/collections/tests/Deque/push_multiple.phpt @@ -0,0 +1,18 @@ +--TEST-- +Collections\Deque constructed from array +--FILE-- +push(); +printf("it=%s count=%d\n", json_encode($it), $it->count()); +$it->push(...range(0, 19)); +printf("it=%s count=%d\n", json_encode($it), $it->count()); +$it->unshift(...range(0, 19)); +printf("it=%s count=%d\n", json_encode($it), $it->count()); +?> +--EXPECT-- +it=[] count=0 +it=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] count=20 +it=[19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] count=40 diff --git a/ext/collections/tests/Deque/push_pop.phpt b/ext/collections/tests/Deque/push_pop.phpt new file mode 100644 index 0000000000000..339eadc9498b0 --- /dev/null +++ b/ext/collections/tests/Deque/push_pop.phpt @@ -0,0 +1,53 @@ +--TEST-- +Collections\Deque push/pop +--FILE-- +getMessage()); + } +} + +echo "Test empty deque\n"; +$it = new Collections\Deque([]); +expect_throws(fn() => $it->pop()); +expect_throws(fn() => $it->pop()); +$it->push(strtoupper('test')); +$it->push(['literal']); +$it->push(new stdClass()); +$it[] = strtoupper('test2'); +$it[] = false; +echo json_encode($it), "\n"; +printf("count=%d\n", count($it)); +var_dump($it->pop()); +var_dump($it->pop()); +var_dump($it->pop()); +var_dump($it->pop()); +echo "After popping 4 elements: ", json_encode($it->toArray()), "\n"; +var_dump($it->pop()); +echo json_encode($it), "\n"; +printf("count=%d\n", count($it)); + +?> +--EXPECT-- +Test empty deque +Caught UnderflowException: Cannot pop from empty Collections\Deque +Caught UnderflowException: Cannot pop from empty Collections\Deque +["TEST",["literal"],{},"TEST2",false] +count=5 +bool(false) +string(5) "TEST2" +object(stdClass)#2 (0) { +} +array(1) { + [0]=> + string(7) "literal" +} +After popping 4 elements: ["TEST"] +string(4) "TEST" +[] +count=0 \ No newline at end of file diff --git a/ext/collections/tests/Deque/push_pop_both.phpt b/ext/collections/tests/Deque/push_pop_both.phpt new file mode 100644 index 0000000000000..20afbdfb617de --- /dev/null +++ b/ext/collections/tests/Deque/push_pop_both.phpt @@ -0,0 +1,45 @@ +--TEST-- +Collections\Deque push/pop/unshift/shift +--FILE-- +getMessage()); + } +} + +function dump_it(Collections\Deque $dq) { + printf("count=%d %s\n", $dq->count(), json_encode($dq)); +} + +$it = new Collections\Deque([]); +dump_it($it); +expect_throws(fn() => $it->pop()); +expect_throws(fn() => $it->shift()); +$it->unshift(strtolower('HELLO')); +dump_it($it); +$it->push(strtolower('WORLD')); +dump_it($it); +foreach ($it as $key => $value) { + printf("%s: %s\n", json_encode($key), json_encode($value)); +} +for ($i = 0; $i < 50; $i++) { + $it->push("y$i"); + $it->unshift("x$i"); +} +dump_it($it); + +?> +--EXPECT-- +count=0 [] +Caught UnderflowException: Cannot pop from empty Collections\Deque +Caught UnderflowException: Cannot shift from empty Collections\Deque +count=1 ["hello"] +count=2 ["hello","world"] +0: "hello" +1: "world" +count=102 ["x49","x48","x47","x46","x45","x44","x43","x42","x41","x40","x39","x38","x37","x36","x35","x34","x33","x32","x31","x30","x29","x28","x27","x26","x25","x24","x23","x22","x21","x20","x19","x18","x17","x16","x15","x14","x13","x12","x11","x10","x9","x8","x7","x6","x5","x4","x3","x2","x1","x0","hello","world","y0","y1","y2","y3","y4","y5","y6","y7","y8","y9","y10","y11","y12","y13","y14","y15","y16","y17","y18","y19","y20","y21","y22","y23","y24","y25","y26","y27","y28","y29","y30","y31","y32","y33","y34","y35","y36","y37","y38","y39","y40","y41","y42","y43","y44","y45","y46","y47","y48","y49"] \ No newline at end of file diff --git a/ext/collections/tests/Deque/reinit_forbidden.phpt b/ext/collections/tests/Deque/reinit_forbidden.phpt new file mode 100644 index 0000000000000..8f47814f60cba --- /dev/null +++ b/ext/collections/tests/Deque/reinit_forbidden.phpt @@ -0,0 +1,29 @@ +--TEST-- +Collections\Deque cannot be re-initialized +--FILE-- +__construct(['first']); + echo "Unexpectedly called constructor\n"; +} catch (Throwable $t) { + printf("Caught %s: %s\n", $t::class, $t->getMessage()); +} +var_dump($it); +try { + $it->__unserialize([new ArrayObject(), new stdClass()]); + echo "Unexpectedly called __unserialize\n"; +} catch (Throwable $t) { + printf("Caught %s: %s\n", $t::class, $t->getMessage()); +} +var_dump($it); +?> +--EXPECT-- +Caught RuntimeException: Called Collections\Deque::__construct twice +object(Collections\Deque)#1 (0) { +} +Caught RuntimeException: Already unserialized +object(Collections\Deque)#1 (0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/serialization.phpt b/ext/collections/tests/Deque/serialization.phpt new file mode 100644 index 0000000000000..2992e224779d5 --- /dev/null +++ b/ext/collections/tests/Deque/serialization.phpt @@ -0,0 +1,34 @@ +--TEST-- +Collections\Deque can be serialized and unserialized +--FILE-- + new stdClass()]); +try { + $it->dynamicProp = 123; +} catch (Throwable $t) { + printf("Caught %s: %s\n", $t::class, $t->getMessage()); +} +$ser = serialize($it); +echo $ser, "\n"; +foreach (unserialize($ser) as $key => $value) { + echo "Entry:\n"; + var_dump($key, $value); +} +var_dump($ser === serialize($it)); +echo "Done\n"; +$x = 123; +$it = new Collections\Deque([]); +var_dump($it->__serialize()); +?> +--EXPECT-- +Caught Error: Cannot create dynamic property Collections\Deque::$dynamicProp +O:17:"Collections\Deque":1:{i:0;O:8:"stdClass":0:{}} +Entry: +int(0) +object(stdClass)#5 (0) { +} +bool(true) +Done +array(0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/set_state.phpt b/ext/collections/tests/Deque/set_state.phpt new file mode 100644 index 0000000000000..9409404fac467 --- /dev/null +++ b/ext/collections/tests/Deque/set_state.phpt @@ -0,0 +1,83 @@ +--TEST-- +Collections\Deque::__set_state +--FILE-- + 'x']); +$it = Collections\Deque::__set_state([strtoupper('a literal'), ['first', 'x'], [(object)['key' => 'value'], null]]); +foreach ($it as $key => $value) { + printf("key=%s value=%s\n", json_encode($key), json_encode($value)); +} +dump_repr($it); +var_dump($it); +var_dump((array)$it); + +?> +--EXPECT-- +\Collections\Deque::__set_state(array( +)) +key=0 value="A LITERAL" +key=1 value=["first","x"] +key=2 value=[{"key":"value"},null] +\Collections\Deque::__set_state(array( + 0 => 'A LITERAL', + 1 => + array ( + 0 => 'first', + 1 => 'x', + ), + 2 => + array ( + 0 => + (object) array( + 'key' => 'value', + ), + 1 => NULL, + ), +)) +object(Collections\Deque)#2 (3) { + [0]=> + string(9) "A LITERAL" + [1]=> + array(2) { + [0]=> + string(5) "first" + [1]=> + string(1) "x" + } + [2]=> + array(2) { + [0]=> + object(stdClass)#1 (1) { + ["key"]=> + string(5) "value" + } + [1]=> + NULL + } +} +array(3) { + [0]=> + string(9) "A LITERAL" + [1]=> + array(2) { + [0]=> + string(5) "first" + [1]=> + string(1) "x" + } + [2]=> + array(2) { + [0]=> + object(stdClass)#1 (1) { + ["key"]=> + string(5) "value" + } + [1]=> + NULL + } +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/shift.phpt b/ext/collections/tests/Deque/shift.phpt new file mode 100644 index 0000000000000..b4eddb562f5a8 --- /dev/null +++ b/ext/collections/tests/Deque/shift.phpt @@ -0,0 +1,13 @@ +--TEST-- +Collections\Deque shift +--FILE-- +shift()); +var_dump($dq[0]); +?> +--EXPECT-- +object(stdClass)#2 (0) { +} +string(4) "TEST" \ No newline at end of file diff --git a/ext/collections/tests/Deque/toArray.phpt b/ext/collections/tests/Deque/toArray.phpt new file mode 100644 index 0000000000000..ff8345f9853ed --- /dev/null +++ b/ext/collections/tests/Deque/toArray.phpt @@ -0,0 +1,27 @@ +--TEST-- +Collections\Deque toArray() +--FILE-- + new stdClass()]); +var_dump($it->toArray()); +var_dump($it->toArray()); +$it = new Collections\Deque([]); +var_dump($it->toArray()); +var_dump($it->toArray()); +?> +--EXPECT-- +array(1) { + [0]=> + object(stdClass)#2 (0) { + } +} +array(1) { + [0]=> + object(stdClass)#2 (0) { + } +} +array(0) { +} +array(0) { +} \ No newline at end of file diff --git a/ext/collections/tests/Deque/top.phpt b/ext/collections/tests/Deque/top.phpt new file mode 100644 index 0000000000000..0d8a710362a5c --- /dev/null +++ b/ext/collections/tests/Deque/top.phpt @@ -0,0 +1,44 @@ +--TEST-- +Collections\Deque last()/first() +--FILE-- +getMessage()); + } +} + +$it = new Collections\Deque(); +expect_throws(fn () => $it->first()); +expect_throws(fn () => $it->last()); +for ($i = 0; $i <= 3; $i++) { + $it->push("last$i"); + $it->unshift("first$i"); +} +echo $it->first(), "\n"; +echo $it->last(), "\n"; +echo "Removing elements\n"; +echo $it->shift(), "\n"; +echo $it->pop(), "\n"; +echo "Inspecting elements after removal\n"; +echo $it->first(), "\n"; +echo $it->last(), "\n"; +printf("count=%d values=%s\n", $it->count(), json_encode($it)); + +?> +--EXPECT-- +Caught UnderflowException: Cannot read first value of empty Collections\Deque +Caught UnderflowException: Cannot read last value of empty Collections\Deque +first3 +last3 +Removing elements +first3 +last3 +Inspecting elements after removal +first2 +last2 +count=6 values=["first2","first1","first0","last0","last1","last2"] \ No newline at end of file diff --git a/ext/collections/tests/Deque/traversable.phpt b/ext/collections/tests/Deque/traversable.phpt new file mode 100644 index 0000000000000..9e78346ef2480 --- /dev/null +++ b/ext/collections/tests/Deque/traversable.phpt @@ -0,0 +1,101 @@ +--TEST-- +Collections\Deque constructed from Traversable +--FILE-- + "s$i"; + } + $o = (object)['key' => 'value']; + yield $o => $o; + yield 0 => 1; + yield 0 => 2; + echo "Done evaluating the generator\n"; +} + +// Collections\Deque eagerly evaluates the passed in Traversable +$it = new Collections\Deque(yields_values()); +foreach ($it as $key => $value) { + printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); +} +echo "Rewind and iterate again starting from r0\n"; +foreach ($it as $key => $value) { + printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); +} +unset($it); + +$emptyIt = new Collections\Deque(new ArrayObject()); +var_dump($emptyIt); +foreach ($emptyIt as $key => $value) { + echo "Unreachable\n"; +} +foreach ($emptyIt as $key => $value) { + echo "Unreachable\n"; +} +echo "Done\n"; + + +?> +--EXPECT-- +Done evaluating the generator +Key: 0 +Value: 's0' +Key: 1 +Value: 's1' +Key: 2 +Value: 's2' +Key: 3 +Value: 's3' +Key: 4 +Value: 's4' +Key: 5 +Value: 's5' +Key: 6 +Value: 's6' +Key: 7 +Value: 's7' +Key: 8 +Value: 's8' +Key: 9 +Value: 's9' +Key: 10 +Value: (object) array( + 'key' => 'value', +) +Key: 11 +Value: 1 +Key: 12 +Value: 2 +Rewind and iterate again starting from r0 +Key: 0 +Value: 's0' +Key: 1 +Value: 's1' +Key: 2 +Value: 's2' +Key: 3 +Value: 's3' +Key: 4 +Value: 's4' +Key: 5 +Value: 's5' +Key: 6 +Value: 's6' +Key: 7 +Value: 's7' +Key: 8 +Value: 's8' +Key: 9 +Value: 's9' +Key: 10 +Value: (object) array( + 'key' => 'value', +) +Key: 11 +Value: 1 +Key: 12 +Value: 2 +object(Collections\Deque)#1 (0) { +} +Done \ No newline at end of file diff --git a/ext/collections/tests/Deque/unserialize.phpt b/ext/collections/tests/Deque/unserialize.phpt new file mode 100644 index 0000000000000..af53730ea2b05 --- /dev/null +++ b/ext/collections/tests/Deque/unserialize.phpt @@ -0,0 +1,17 @@ +--TEST-- +Collections\Deque unserialize error handling +--FILE-- + 'second']); + $ser = 'O:17:"Collections\Deque":2:{i:0;s:5:"first";s:5:"unexp";s:6:"second";}'; + try { + unserialize($ser); + } catch (Throwable $e) { + printf("Caught %s: %s\n", $e::class, $e->getMessage()); + } +}); +?> +--EXPECT-- +Caught UnexpectedValueException: Collections\Deque::__unserialize saw unexpected string key, expected sequence of values diff --git a/ext/collections/tests/Deque/unshift.phpt b/ext/collections/tests/Deque/unshift.phpt new file mode 100644 index 0000000000000..b68fe9ec2c858 --- /dev/null +++ b/ext/collections/tests/Deque/unshift.phpt @@ -0,0 +1,22 @@ +--TEST-- +Collections\Deque unshift +--FILE-- +unshift("$i"); +} +foreach ($it as $value) { + echo "$value,"; +} +echo "\n"; +$values = []; +while (count($it) > 0) { + $values[] = $it->shift(); +} +echo json_encode($values), "\n"; +?> +--EXPECT-- +19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0, +["19","18","17","16","15","14","13","12","11","10","9","8","7","6","5","4","3","2","1","0"] \ No newline at end of file diff --git a/ext/collections/tests/Deque/var_export_recursion.phpt b/ext/collections/tests/Deque/var_export_recursion.phpt new file mode 100644 index 0000000000000..bcbe0ef324e47 --- /dev/null +++ b/ext/collections/tests/Deque/var_export_recursion.phpt @@ -0,0 +1,38 @@ +--TEST-- +Collections\Deque: Handle edge cases in var_export for circular data +--INI-- +error_reporting=E_ALL +display_errors=stderr +--FILE-- + +--EXPECTF-- +Warning: var_export does not handle circular references in %s on line 8 +Warning: var_export does not handle circular references in %s on line 8 +%SCollections\Deque::__set_state(array( + 0 => NAN, + 1 => 0.0, + 2 => NULL, + 3 => NULL, +)) +object(Collections\Deque)#2 (4) refcount(6){ + [0]=> + float(NAN) + [1]=> + float(-0) + [2]=> + *RECURSION* + [3]=> + *RECURSION* +} \ No newline at end of file diff --git a/ext/spl/config.m4 b/ext/spl/config.m4 index 589e8681d70ec..b2d17c4b70469 100644 --- a/ext/spl/config.m4 +++ b/ext/spl/config.m4 @@ -1,5 +1,5 @@ PHP_NEW_EXTENSION(spl, php_spl.c spl_functions.c spl_iterators.c spl_array.c spl_directory.c spl_exceptions.c spl_observer.c spl_dllist.c spl_heap.c spl_fixedarray.c, no,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) -PHP_INSTALL_HEADERS([ext/spl], [php_spl.h spl_array.h spl_directory.h spl_engine.h spl_exceptions.h spl_functions.h spl_iterators.h spl_observer.h spl_dllist.h spl_heap.h spl_fixedarray.h]) +PHP_INSTALL_HEADERS([ext/spl], [php_spl.h spl_array.h spl_directory.h spl_engine.h spl_exceptions.h spl_functions.h spl_iterators.h spl_observer.h spl_dllist.h spl_heap.h spl_fixedarray.h spl_util.h]) PHP_ADD_EXTENSION_DEP(spl, pcre, true) PHP_ADD_EXTENSION_DEP(spl, standard, true) PHP_ADD_EXTENSION_DEP(spl, json) diff --git a/ext/spl/config.w32 b/ext/spl/config.w32 index 92659aa86d2ab..95940f0a98eca 100644 --- a/ext/spl/config.w32 +++ b/ext/spl/config.w32 @@ -2,4 +2,4 @@ EXTENSION("spl", "php_spl.c spl_functions.c spl_iterators.c spl_array.c spl_directory.c spl_exceptions.c spl_observer.c spl_dllist.c spl_heap.c spl_fixedarray.c", false /*never shared */, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); PHP_SPL="yes"; -PHP_INSTALL_HEADERS("ext/spl", "php_spl.h spl_array.h spl_directory.h spl_engine.h spl_exceptions.h spl_functions.h spl_iterators.h spl_observer.h spl_dllist.h spl_heap.h spl_fixedarray.h"); +PHP_INSTALL_HEADERS("ext/spl", "php_spl.h spl_array.h spl_directory.h spl_engine.h spl_exceptions.h spl_functions.h spl_iterators.h spl_observer.h spl_dllist.h spl_heap.h spl_fixedarray.h spl_util.h"); diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c index 465c649e980aa..37549c8ea2d6c 100644 --- a/ext/spl/spl_fixedarray.c +++ b/ext/spl/spl_fixedarray.c @@ -31,6 +31,7 @@ #include "spl_fixedarray.h" #include "spl_exceptions.h" #include "spl_iterators.h" +#include "spl_util.h" #include "ext/json/php_json.h" zend_object_handlers spl_handler_SplFixedArray; @@ -306,37 +307,6 @@ static zend_object *spl_fixedarray_object_clone(zend_object *old_object) return new_object; } -static zend_long spl_offset_convert_to_long(zval *offset) /* {{{ */ -{ - try_again: - switch (Z_TYPE_P(offset)) { - case IS_STRING: { - zend_ulong index; - if (ZEND_HANDLE_NUMERIC(Z_STR_P(offset), index)) { - return (zend_long) index; - } - break; - } - case IS_DOUBLE: - return zend_dval_to_lval_safe(Z_DVAL_P(offset)); - case IS_LONG: - return Z_LVAL_P(offset); - case IS_FALSE: - return 0; - case IS_TRUE: - return 1; - case IS_REFERENCE: - offset = Z_REFVAL_P(offset); - goto try_again; - case IS_RESOURCE: - zend_use_resource_as_offset(offset); - return Z_RES_HANDLE_P(offset); - } - - zend_type_error("Illegal offset type"); - return 0; -} - static zval *spl_fixedarray_object_read_dimension_helper(spl_fixedarray_object *intern, zval *offset) { zend_long index; diff --git a/ext/spl/spl_util.h b/ext/spl/spl_util.h new file mode 100644 index 0000000000000..99766b269847b --- /dev/null +++ b/ext/spl/spl_util.h @@ -0,0 +1,53 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Antony Dovgal | + | Etienne Kneuss | + +----------------------------------------------------------------------+ +*/ +#ifndef SPL_UTIL +#define SPL_UTIL + +#include "zend_types.h" + +static zend_always_inline zend_long spl_offset_convert_to_long(zval *offset) /* {{{ */ +{ + try_again: + switch (Z_TYPE_P(offset)) { + case IS_STRING: { + zend_ulong index; + if (ZEND_HANDLE_NUMERIC(Z_STR_P(offset), index)) { + return (zend_long) index; + } + break; + } + case IS_DOUBLE: + return zend_dval_to_lval_safe(Z_DVAL_P(offset)); + case IS_LONG: + return Z_LVAL_P(offset); + case IS_FALSE: + return 0; + case IS_TRUE: + return 1; + case IS_REFERENCE: + offset = Z_REFVAL_P(offset); + goto try_again; + case IS_RESOURCE: + zend_use_resource_as_offset(offset); + return Z_RES_HANDLE_P(offset); + } + + zend_type_error("Illegal offset type"); + return 0; +} + +#endif From 26a777de9705b31e05ac8650d097eb96f72b30ab Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Thu, 10 Nov 2022 08:30:11 -0500 Subject: [PATCH 2/3] Optimize insert() and offsetUnset() with offsets near front of Deque --- ext/collections/collections_deque.c | 87 +++++++++++++------ ext/collections/tests/Deque/insert.phpt | 24 +++++ .../tests/Deque/insert_remove_front.phpt | 19 ++++ 3 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 ext/collections/tests/Deque/insert.phpt create mode 100644 ext/collections/tests/Deque/insert_remove_front.phpt diff --git a/ext/collections/collections_deque.c b/ext/collections/collections_deque.c index 1d8c82c3da902..541ad24f36f18 100644 --- a/ext/collections/collections_deque.c +++ b/ext/collections/collections_deque.c @@ -1086,25 +1086,45 @@ static zend_always_inline void collections_deque_entries_insert_values(collectio collections_deque_entries_raise_capacity(array, new_capacity); mask = array->mask; } - const uint32_t offset = array->offset; + uint32_t offset = array->offset; zval *const circular_buffer = array->circular_buffer; collections_deque_maybe_adjust_iterators_before_insert(array, inserted_offset, argc); - /* Move elements to the end of the deque */ - /* TODO move the start instead when there are less elements. */ - uint32_t src_offset = (offset + old_size) & mask; /* Masked in do-while loop. */ - uint32_t dst_offset = src_offset + argc; - const uint32_t src_end = (offset + inserted_offset) & mask; - - while (src_offset != src_end) { - src_offset = (src_offset - 1) & mask; - dst_offset = (dst_offset - 1) & mask; - ZEND_ASSERT(Z_TYPE(circular_buffer[src_offset]) != IS_UNDEF); - ZVAL_COPY_VALUE(&circular_buffer[dst_offset], &circular_buffer[src_offset]); + if (inserted_offset >= (old_size / 2)) { + /* The inserted_offset is closer to the end of the Deque. */ + /* Move elements to the end of the deque. */ + uint32_t src_offset = (offset + old_size) & mask; /* Start copying from the end of the deque forward to create a gap */ + uint32_t dst_offset = src_offset + argc; /* Masked in do-while loop. */ + const uint32_t src_end = (offset + inserted_offset) & mask; + + while (src_offset != src_end) { + src_offset = (src_offset - 1) & mask; + dst_offset = (dst_offset - 1) & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[src_offset]) != IS_UNDEF); + ZVAL_COPY_VALUE(&circular_buffer[dst_offset], &circular_buffer[src_offset]); + } + } else { + /* The inserted_offset is closer to the start of the Deque. */ + /* Move the offset of front of the deque backwards and move elements to the front of the deque. */ + uint32_t src_offset = offset & mask; /* Start copying from the start of the deque backward to create a gap */ + uint32_t dst_offset = (offset - argc) & mask; /* Masked in do-while loop. New start offset of the front of the deque */ + const uint32_t src_end = (src_offset + inserted_offset) & mask; + /* Adjust the offset of the front of the deque */ + offset = dst_offset; + array->offset = dst_offset; + + /* Move elements to the front of the deque when the insertion offset is closer to the front */ + while (src_offset != src_end) { + ZEND_ASSERT(Z_TYPE(circular_buffer[src_offset]) != IS_UNDEF); + ZVAL_COPY_VALUE(&circular_buffer[dst_offset], &circular_buffer[src_offset]); + src_offset = (src_offset + 1) & mask; + dst_offset = (dst_offset + 1) & mask; + } } - dst_offset = (offset + inserted_offset) & mask; + /* Insert 'argc' elements in the gap that was created */ + uint32_t dst_offset = (offset + inserted_offset) & mask; do { zval *dest = &circular_buffer[dst_offset]; ZVAL_COPY(dest, args); @@ -1153,22 +1173,39 @@ static zend_always_inline void collections_deque_entries_remove_offset(collectio const uint32_t offset = array->offset; zval *const circular_buffer = array->circular_buffer; - /* Move elements from the end of the deque to replace the removed element */ - /* TODO: Remove from the front instead if there are fewer elements to remove, adjust iterators */ uint32_t it_offset = (offset + removed_offset) & mask; + zval removed_val; collections_deque_maybe_adjust_iterators_before_remove(array, removed_offset); - - zval removed_val; ZVAL_COPY_VALUE(&removed_val, &circular_buffer[it_offset]); - const uint32_t it_end = (offset + old_size - 1) & mask; - ZEND_ASSERT(Z_TYPE(circular_buffer[it_offset]) != IS_UNDEF); - - while (it_offset != it_end) { - const uint32_t next_offset = (it_offset + 1) & mask; - ZEND_ASSERT(Z_TYPE(circular_buffer[next_offset]) != IS_UNDEF); - ZVAL_COPY_VALUE(&circular_buffer[it_offset], &circular_buffer[next_offset]); - it_offset = next_offset; + + if (removed_offset >= old_size / 2) { + /* The removed offset is closer to the end of the Deque. */ + /* Move elements from the end of the deque backwards to replace the removed element */ + const uint32_t it_end = (offset + old_size - 1) & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[it_offset]) != IS_UNDEF); + + while (it_offset != it_end) { + const uint32_t next_offset = (it_offset + 1) & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[next_offset]) != IS_UNDEF); + ZVAL_COPY_VALUE(&circular_buffer[it_offset], &circular_buffer[next_offset]); + it_offset = next_offset; + } + } else { + /* The removed offset is closer to the end of the Deque. */ + /* Move elements from the front of the Deque forwards to replace the removed element */ + const uint32_t it_end = offset & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[it_offset]) != IS_UNDEF); + + /* Move the start of the deque forward */ + array->offset = (offset + 1) & mask; + + while (it_offset != it_end) { + const uint32_t next_offset = (it_offset - 1) & mask; + ZEND_ASSERT(Z_TYPE(circular_buffer[next_offset]) != IS_UNDEF); + ZVAL_COPY_VALUE(&circular_buffer[it_offset], &circular_buffer[next_offset]); + it_offset = next_offset; + } } const uint32_t new_size = old_size - 1; diff --git a/ext/collections/tests/Deque/insert.phpt b/ext/collections/tests/Deque/insert.phpt new file mode 100644 index 0000000000000..9457285c6c27f --- /dev/null +++ b/ext/collections/tests/Deque/insert.phpt @@ -0,0 +1,24 @@ +--TEST-- +Collections\Deque insert and remove at offset +--FILE-- +insert(0, "{$prefix}2"); +$a->insert(0, "{$prefix}0", "{$prefix}1"); +echo json_encode($a), "\n"; +$a->insert(3, 'third', "{$prefix}4"); +echo json_encode($a), "\n"; +unset($a[2]); +while (count($a) > 0) { + unset($a[0]); + echo json_encode($a), "\n"; +} +?> +--EXPECT-- +["v0","v1","v2"] +["v0","v1","v2","third","v4"] +["v1","third","v4"] +["third","v4"] +["v4"] +[] diff --git a/ext/collections/tests/Deque/insert_remove_front.phpt b/ext/collections/tests/Deque/insert_remove_front.phpt new file mode 100644 index 0000000000000..3c82f076eb57a --- /dev/null +++ b/ext/collections/tests/Deque/insert_remove_front.phpt @@ -0,0 +1,19 @@ +--TEST-- +Collections\Deque insert and remove at front offset is efficient +--FILE-- +insert(0, $i % 10); +} +$total = 0; +while (count($deque) > 0) { + $total += $deque[0]; + unset($deque[0]); +} +printf("total: %d\n", $total); +?> +--EXPECT-- +total: 45000 From c2bad593dcdc699f936909e45907abd3a0340d62 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Fri, 11 Nov 2022 19:43:45 -0500 Subject: [PATCH 3/3] Rename shift/unshift to popFront/pushFront These operations are constant-time. Unlike array_shift/array_unshift, they aren't actually shifting values in the representation, and the new names are more self-explanatory and commonly used in other Deque implementations. --- ext/collections/collections_deque.c | 6 ++--- ext/collections/collections_deque.stub.php | 16 ++++++------- ext/collections/collections_deque_arginfo.h | 14 +++++------ ext/collections/tests/Deque/foreach.phpt | 24 +++++++++---------- ext/collections/tests/Deque/iterator.phpt | 10 ++++---- .../tests/Deque/offsetGetShifted.phpt | 2 +- ext/collections/tests/Deque/popFront.phpt | 6 ++--- ext/collections/tests/Deque/popMany.phpt | 6 ++--- ext/collections/tests/Deque/pushFront.phpt | 8 +++---- .../tests/Deque/push_multiple.phpt | 2 +- .../tests/Deque/push_pop_both.phpt | 12 +++++----- ext/collections/tests/Deque/shift.phpt | 6 ++--- ext/collections/tests/Deque/top.phpt | 6 ++--- ext/collections/tests/Deque/unshift.phpt | 8 +++---- 14 files changed, 63 insertions(+), 63 deletions(-) diff --git a/ext/collections/collections_deque.c b/ext/collections/collections_deque.c index 541ad24f36f18..520a24349331a 100644 --- a/ext/collections/collections_deque.c +++ b/ext/collections/collections_deque.c @@ -1029,7 +1029,7 @@ static zend_always_inline void collections_deque_maybe_adjust_iterators_before_i } } -PHP_METHOD(Collections_Deque, unshift) +PHP_METHOD(Collections_Deque, pushFront) { const zval *args; uint32_t argc; @@ -1281,7 +1281,7 @@ PHP_METHOD(Collections_Deque, last) RETVAL_COPY(collections_deque_get_entry_at_offset(&intern->array, old_size - 1)); } -PHP_METHOD(Collections_Deque, shift) +PHP_METHOD(Collections_Deque, popFront) { ZEND_PARSE_PARAMETERS_NONE(); @@ -1289,7 +1289,7 @@ PHP_METHOD(Collections_Deque, shift) DEBUG_ASSERT_CONSISTENT_DEQUE(array); const uint32_t old_size = array->size; if (old_size == 0) { - zend_throw_exception(spl_ce_UnderflowException, "Cannot shift from empty Collections\\Deque", 0); + zend_throw_exception(spl_ce_UnderflowException, "Cannot popFront from empty Collections\\Deque", 0); RETURN_THROWS(); } collections_deque_maybe_adjust_iterators_before_remove(array, 0); diff --git a/ext/collections/collections_deque.stub.php b/ext/collections/collections_deque.stub.php index a8de7361b55e3..29f80afccdeaf 100644 --- a/ext/collections/collections_deque.stub.php +++ b/ext/collections/collections_deque.stub.php @@ -16,19 +16,19 @@ * This supports amortized constant time pushing and popping onto the front or back of the Deque. * * Naming is based on https://www.php.net/spldoublylinkedlist - * and on array_push/pop/unshift/shift and array_key_first. + * and on array_push/pop/pushFront/popFront and array_key_first. */ final class Deque implements \IteratorAggregate, \Countable, \JsonSerializable, \ArrayAccess { /** Construct the Deque from the values of the Traversable/array, ignoring keys */ public function __construct(iterable $iterator = []) {} /** - * Returns an iterator that accounts for calls to shift/unshift tracking the position of the start of the Deque. - * Calls to shift/unshift will do the following: + * Returns an iterator that accounts for calls to popFront/pushFront tracking the position of the start of the Deque. + * Calls to popFront/pushFront will do the following: * - Increase/Decrease the value returned by the iterator's key() * by the number of elements added/removed to/from the start 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 + * - Repeated calls to popFront 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. */ @@ -48,18 +48,18 @@ public static function __set_state(array $array): Deque {} /** Appends value(s) to the end of the Deque, like array_push. */ public function push(mixed ...$values): void {} - /** Prepends value(s) to the start of the Deque, like array_unshift. */ - public function unshift(mixed ...$values): void {} + /** Prepends value(s) to the start of the Deque, like array_pushFront. */ + public function pushFront(mixed ...$values): void {} /** * Pops a value from the end of the Deque. * @throws \UnderflowException if the Deque is empty */ public function pop(): mixed {} /** - * Shifts a value from the start of the Deque. + * Pops a value from the start of the Deque. * @throws \UnderflowException if the Deque is empty */ - public function shift(): mixed {} + public function popFront(): mixed {} /** * Peeks at the value at the start of the Deque. diff --git a/ext/collections/collections_deque_arginfo.h b/ext/collections/collections_deque_arginfo.h index 9ba04d7e99496..50b102121d912 100644 --- a/ext/collections/collections_deque_arginfo.h +++ b/ext/collections/collections_deque_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 75aa473d9be94d2b02515dad1eed3b44dd850e47 */ + * Stub hash: a6e06241621b8552c7f12b33f9a5311672ad09b9 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Collections_Deque___construct, 0, 0, 0) ZEND_ARG_OBJ_TYPE_MASK(0, iterator, Traversable, MAY_BE_ARRAY, "[]") @@ -32,12 +32,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_push, 0, ZEND_ARG_VARIADIC_TYPE_INFO(0, values, IS_MIXED, 0) ZEND_END_ARG_INFO() -#define arginfo_class_Collections_Deque_unshift arginfo_class_Collections_Deque_push +#define arginfo_class_Collections_Deque_pushFront arginfo_class_Collections_Deque_push ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Collections_Deque_pop, 0, 0, IS_MIXED, 0) ZEND_END_ARG_INFO() -#define arginfo_class_Collections_Deque_shift arginfo_class_Collections_Deque_pop +#define arginfo_class_Collections_Deque_popFront arginfo_class_Collections_Deque_pop #define arginfo_class_Collections_Deque_first arginfo_class_Collections_Deque_pop @@ -79,9 +79,9 @@ ZEND_METHOD(Collections_Deque, toArray); ZEND_METHOD(Collections_Deque, __unserialize); ZEND_METHOD(Collections_Deque, __set_state); ZEND_METHOD(Collections_Deque, push); -ZEND_METHOD(Collections_Deque, unshift); +ZEND_METHOD(Collections_Deque, pushFront); ZEND_METHOD(Collections_Deque, pop); -ZEND_METHOD(Collections_Deque, shift); +ZEND_METHOD(Collections_Deque, popFront); ZEND_METHOD(Collections_Deque, first); ZEND_METHOD(Collections_Deque, last); ZEND_METHOD(Collections_Deque, insert); @@ -101,9 +101,9 @@ static const zend_function_entry class_Collections_Deque_methods[] = { ZEND_ME(Collections_Deque, __unserialize, arginfo_class_Collections_Deque___unserialize, ZEND_ACC_PUBLIC) ZEND_ME(Collections_Deque, __set_state, arginfo_class_Collections_Deque___set_state, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(Collections_Deque, push, arginfo_class_Collections_Deque_push, ZEND_ACC_PUBLIC) - ZEND_ME(Collections_Deque, unshift, arginfo_class_Collections_Deque_unshift, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, pushFront, arginfo_class_Collections_Deque_pushFront, ZEND_ACC_PUBLIC) ZEND_ME(Collections_Deque, pop, arginfo_class_Collections_Deque_pop, ZEND_ACC_PUBLIC) - ZEND_ME(Collections_Deque, shift, arginfo_class_Collections_Deque_shift, ZEND_ACC_PUBLIC) + ZEND_ME(Collections_Deque, popFront, arginfo_class_Collections_Deque_popFront, ZEND_ACC_PUBLIC) ZEND_ME(Collections_Deque, first, arginfo_class_Collections_Deque_first, ZEND_ACC_PUBLIC) ZEND_ME(Collections_Deque, last, arginfo_class_Collections_Deque_last, ZEND_ACC_PUBLIC) ZEND_ME(Collections_Deque, toArray, arginfo_class_Collections_Deque_toArray, ZEND_ACC_PUBLIC) diff --git a/ext/collections/tests/Deque/foreach.phpt b/ext/collections/tests/Deque/foreach.phpt index 97055ef741a08..927e7c58dcab4 100644 --- a/ext/collections/tests/Deque/foreach.phpt +++ b/ext/collections/tests/Deque/foreach.phpt @@ -9,27 +9,27 @@ function mut(string $s) { return $s; } -echo "Test push/unshift\n"; +echo "Test push/pushFront\n"; $deque = new Collections\Deque(['a', 'b']); foreach ($deque as $key => $value) { if (strlen($value) === 1) { $deque->push("{$value}_"); - $deque->unshift("_$value"); + $deque->pushFront("_$value"); } printf("Key: %s Value: %s\n", var_export($key, true), var_export($value, true)); } var_dump($deque); -echo "Test shift\n"; +echo "Test popFront\n"; foreach ($deque as $key => $value) { echo "Shifting $key $value\n"; - var_dump($deque->shift()); + var_dump($deque->popFront()); } -echo "Test shift out of bounds\n"; +echo "Test popFront out of bounds\n"; $deque = new Collections\Deque([mut('a1'), mut('b1'), mut('c1'), mut('d1')]); foreach ($deque as $key => $value) { - var_dump($deque->shift()); - var_dump($deque->shift()); + var_dump($deque->popFront()); + var_dump($deque->popFront()); echo "Saw $key: $value\n"; // iteration does not stop early, iterator points to just before start of Deque } @@ -39,18 +39,18 @@ echo "Test iteration behavior\n"; $deque = new Collections\Deque([mut('a1'), mut('a2')]); $it = $deque->getIterator(); echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n"; -$deque->shift(); +$deque->popFront(); // invalid, outside the range of the deque echo json_encode(['valid' => $it->valid(), 'key' => $it->key()]), "\n"; $it->next(); echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n"; -$deque->unshift('a', 'b'); +$deque->pushFront('a', 'b'); unset($deque); echo json_encode(['valid' => $it->valid(), 'key' => $it->key(), 'value' => $it->current()]), "\n"; ?> --EXPECT-- -Test push/unshift +Test push/pushFront Key: 0 Value: 'a' Key: 2 Value: 'b' Key: 4 Value: 'a_' @@ -69,7 +69,7 @@ object(Collections\Deque)#1 (6) { [5]=> string(2) "b_" } -Test shift +Test popFront Shifting 0 _b string(2) "_b" Shifting 0 _a @@ -82,7 +82,7 @@ Shifting 0 a_ string(2) "a_" Shifting 0 b_ string(2) "b_" -Test shift out of bounds +Test popFront out of bounds string(2) "a1" string(2) "b1" Saw 0: a1 diff --git a/ext/collections/tests/Deque/iterator.phpt b/ext/collections/tests/Deque/iterator.phpt index 321d227dad215..a0b6cc45045d4 100644 --- a/ext/collections/tests/Deque/iterator.phpt +++ b/ext/collections/tests/Deque/iterator.phpt @@ -19,12 +19,12 @@ var_dump($it->key()); var_dump($it->current()); var_dump($it->next()); var_dump($it->valid()); -echo "After shift\n"; -$dq->shift(); +echo "After popFront\n"; +$dq->popFront(); var_dump($it->key()); var_dump($it->current()); var_dump($it->valid()); -$dq->shift(); +$dq->popFront(); var_dump($it->key()); // null for invalid iterator expect_throws(fn() => $it->current()); var_dump($it->valid()); @@ -39,7 +39,7 @@ object(stdClass)#2 (0) { } NULL bool(true) -After shift +After popFront int(0) string(4) "TEST" bool(true) @@ -47,4 +47,4 @@ NULL Caught OutOfBoundsException: Index out of range bool(false) object(InternalIterator)#4 (0) { -} \ No newline at end of file +} diff --git a/ext/collections/tests/Deque/offsetGetShifted.phpt b/ext/collections/tests/Deque/offsetGetShifted.phpt index e491ecc2be3c0..6566769f9dff4 100644 --- a/ext/collections/tests/Deque/offsetGetShifted.phpt +++ b/ext/collections/tests/Deque/offsetGetShifted.phpt @@ -15,7 +15,7 @@ $it = new Collections\Deque(); for ($i = 0; $i < 7; $i++) { $it->push("x$i"); } -var_dump($it->shift()); +var_dump($it->popFront()); $it->push('new'); $it->push('another'); for ($i = 0; $i < count($it); $i++) { diff --git a/ext/collections/tests/Deque/popFront.phpt b/ext/collections/tests/Deque/popFront.phpt index b4eddb562f5a8..bc8dc36694cf5 100644 --- a/ext/collections/tests/Deque/popFront.phpt +++ b/ext/collections/tests/Deque/popFront.phpt @@ -1,13 +1,13 @@ --TEST-- -Collections\Deque shift +Collections\Deque popFront --FILE-- shift()); +var_dump($dq->popFront()); var_dump($dq[0]); ?> --EXPECT-- object(stdClass)#2 (0) { } -string(4) "TEST" \ No newline at end of file +string(4) "TEST" diff --git a/ext/collections/tests/Deque/popMany.phpt b/ext/collections/tests/Deque/popMany.phpt index ea6f43102669e..097e25defbc9a 100644 --- a/ext/collections/tests/Deque/popMany.phpt +++ b/ext/collections/tests/Deque/popMany.phpt @@ -7,11 +7,11 @@ $dq = new Collections\Deque(); for ($i = 0; $i < 8; $i++) { $dq->push("v$i"); } -$dq->shift(); +$dq->popFront(); $dq->push("extra"); printf("dq=%s count=%d\n", json_encode($dq), $dq->count()); for ($i = 0; $i < 7; $i++) { - $value = $dq->shift(); + $value = $dq->popFront(); printf("popped %s\n", $value); } // Collections\Deque should reclaim memory once roughly a quarter of the memory is actually used. @@ -37,4 +37,4 @@ object(Collections\Deque)#1 (1) { } dq=[] count=0 object(Collections\Deque)#1 (0) { -} \ No newline at end of file +} diff --git a/ext/collections/tests/Deque/pushFront.phpt b/ext/collections/tests/Deque/pushFront.phpt index b68fe9ec2c858..3a6b40e831cfd 100644 --- a/ext/collections/tests/Deque/pushFront.phpt +++ b/ext/collections/tests/Deque/pushFront.phpt @@ -1,11 +1,11 @@ --TEST-- -Collections\Deque unshift +Collections\Deque pushFront --FILE-- unshift("$i"); + $it->pushFront("$i"); } foreach ($it as $value) { echo "$value,"; @@ -13,10 +13,10 @@ foreach ($it as $value) { echo "\n"; $values = []; while (count($it) > 0) { - $values[] = $it->shift(); + $values[] = $it->popFront(); } echo json_encode($values), "\n"; ?> --EXPECT-- 19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0, -["19","18","17","16","15","14","13","12","11","10","9","8","7","6","5","4","3","2","1","0"] \ No newline at end of file +["19","18","17","16","15","14","13","12","11","10","9","8","7","6","5","4","3","2","1","0"] diff --git a/ext/collections/tests/Deque/push_multiple.phpt b/ext/collections/tests/Deque/push_multiple.phpt index 33c18585fb9d6..19ad125cfa0d6 100644 --- a/ext/collections/tests/Deque/push_multiple.phpt +++ b/ext/collections/tests/Deque/push_multiple.phpt @@ -9,7 +9,7 @@ $it->push(); printf("it=%s count=%d\n", json_encode($it), $it->count()); $it->push(...range(0, 19)); printf("it=%s count=%d\n", json_encode($it), $it->count()); -$it->unshift(...range(0, 19)); +$it->pushFront(...range(0, 19)); printf("it=%s count=%d\n", json_encode($it), $it->count()); ?> --EXPECT-- diff --git a/ext/collections/tests/Deque/push_pop_both.phpt b/ext/collections/tests/Deque/push_pop_both.phpt index 20afbdfb617de..9a5eb8f776049 100644 --- a/ext/collections/tests/Deque/push_pop_both.phpt +++ b/ext/collections/tests/Deque/push_pop_both.phpt @@ -1,5 +1,5 @@ --TEST-- -Collections\Deque push/pop/unshift/shift +Collections\Deque push/pop/pushFront/popFront --FILE-- $it->pop()); -expect_throws(fn() => $it->shift()); -$it->unshift(strtolower('HELLO')); +expect_throws(fn() => $it->popFront()); +$it->pushFront(strtolower('HELLO')); dump_it($it); $it->push(strtolower('WORLD')); dump_it($it); @@ -29,7 +29,7 @@ foreach ($it as $key => $value) { } for ($i = 0; $i < 50; $i++) { $it->push("y$i"); - $it->unshift("x$i"); + $it->pushFront("x$i"); } dump_it($it); @@ -37,9 +37,9 @@ dump_it($it); --EXPECT-- count=0 [] Caught UnderflowException: Cannot pop from empty Collections\Deque -Caught UnderflowException: Cannot shift from empty Collections\Deque +Caught UnderflowException: Cannot popFront from empty Collections\Deque count=1 ["hello"] count=2 ["hello","world"] 0: "hello" 1: "world" -count=102 ["x49","x48","x47","x46","x45","x44","x43","x42","x41","x40","x39","x38","x37","x36","x35","x34","x33","x32","x31","x30","x29","x28","x27","x26","x25","x24","x23","x22","x21","x20","x19","x18","x17","x16","x15","x14","x13","x12","x11","x10","x9","x8","x7","x6","x5","x4","x3","x2","x1","x0","hello","world","y0","y1","y2","y3","y4","y5","y6","y7","y8","y9","y10","y11","y12","y13","y14","y15","y16","y17","y18","y19","y20","y21","y22","y23","y24","y25","y26","y27","y28","y29","y30","y31","y32","y33","y34","y35","y36","y37","y38","y39","y40","y41","y42","y43","y44","y45","y46","y47","y48","y49"] \ No newline at end of file +count=102 ["x49","x48","x47","x46","x45","x44","x43","x42","x41","x40","x39","x38","x37","x36","x35","x34","x33","x32","x31","x30","x29","x28","x27","x26","x25","x24","x23","x22","x21","x20","x19","x18","x17","x16","x15","x14","x13","x12","x11","x10","x9","x8","x7","x6","x5","x4","x3","x2","x1","x0","hello","world","y0","y1","y2","y3","y4","y5","y6","y7","y8","y9","y10","y11","y12","y13","y14","y15","y16","y17","y18","y19","y20","y21","y22","y23","y24","y25","y26","y27","y28","y29","y30","y31","y32","y33","y34","y35","y36","y37","y38","y39","y40","y41","y42","y43","y44","y45","y46","y47","y48","y49"] diff --git a/ext/collections/tests/Deque/shift.phpt b/ext/collections/tests/Deque/shift.phpt index b4eddb562f5a8..bc8dc36694cf5 100644 --- a/ext/collections/tests/Deque/shift.phpt +++ b/ext/collections/tests/Deque/shift.phpt @@ -1,13 +1,13 @@ --TEST-- -Collections\Deque shift +Collections\Deque popFront --FILE-- shift()); +var_dump($dq->popFront()); var_dump($dq[0]); ?> --EXPECT-- object(stdClass)#2 (0) { } -string(4) "TEST" \ No newline at end of file +string(4) "TEST" diff --git a/ext/collections/tests/Deque/top.phpt b/ext/collections/tests/Deque/top.phpt index 0d8a710362a5c..8731a97fa3d40 100644 --- a/ext/collections/tests/Deque/top.phpt +++ b/ext/collections/tests/Deque/top.phpt @@ -17,12 +17,12 @@ expect_throws(fn () => $it->first()); expect_throws(fn () => $it->last()); for ($i = 0; $i <= 3; $i++) { $it->push("last$i"); - $it->unshift("first$i"); + $it->pushFront("first$i"); } echo $it->first(), "\n"; echo $it->last(), "\n"; echo "Removing elements\n"; -echo $it->shift(), "\n"; +echo $it->popFront(), "\n"; echo $it->pop(), "\n"; echo "Inspecting elements after removal\n"; echo $it->first(), "\n"; @@ -41,4 +41,4 @@ last3 Inspecting elements after removal first2 last2 -count=6 values=["first2","first1","first0","last0","last1","last2"] \ No newline at end of file +count=6 values=["first2","first1","first0","last0","last1","last2"] diff --git a/ext/collections/tests/Deque/unshift.phpt b/ext/collections/tests/Deque/unshift.phpt index b68fe9ec2c858..3a6b40e831cfd 100644 --- a/ext/collections/tests/Deque/unshift.phpt +++ b/ext/collections/tests/Deque/unshift.phpt @@ -1,11 +1,11 @@ --TEST-- -Collections\Deque unshift +Collections\Deque pushFront --FILE-- unshift("$i"); + $it->pushFront("$i"); } foreach ($it as $value) { echo "$value,"; @@ -13,10 +13,10 @@ foreach ($it as $value) { echo "\n"; $values = []; while (count($it) > 0) { - $values[] = $it->shift(); + $values[] = $it->popFront(); } echo json_encode($values), "\n"; ?> --EXPECT-- 19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0, -["19","18","17","16","15","14","13","12","11","10","9","8","7","6","5","4","3","2","1","0"] \ No newline at end of file +["19","18","17","16","15","14","13","12","11","10","9","8","7","6","5","4","3","2","1","0"]