From a224a897f8cbf263ab941f8a887b3a24bac23e8b Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Sun, 12 Sep 2021 11:58:35 -0400 Subject: [PATCH 1/7] Proposal: Add `final class Vector` to PHP See spl_vector.stub.php for the userland API. Earlier work on the implementation can be found at https://github.com/TysonAndre/pecl-teds and it can be tested out at https://pecl.php.net/package/teds as `Teds\Vector` (currently the same apart from reusing spl's conversion of mixed to `int $offset`) This was originally based on spl_fixedarray.c and previous work I did on an RFC. Notable features: - Roughly half the memory usage of (non-constant) arrays due to not needing a list of indexes of buckets separately from the zval buckets themselves. - Same memory usage as SplFixedArray in 8.2 for a given **capacity** - Lower memory usage and better performance than SplDoublyLinkedList or its subclasses (SplStack) due to being an array instead of a linked list - More efficient resizing than SplFixedArray's setSize(getSize+1) ```php final class Vector implements IteratorAggregate, Countable, JsonSerializable, ArrayAccess { public function __construct( iterable $iterator = [], bool $preserveKeys = true ) {} public function getIterator(): InternalIterator {} public function count(): int {} public function capacity(): int {} public function clear(): void {} public function setSize(int $size): void {} public function __serialize(): array {} public function __unserialize(array $data): void {} public static function __set_state(array $array): Vector {} public function push(mixed $value): void {} public function pop(): mixed {} public function toArray(): array {} // Strictly typed, unlike offsetGet/offsetSet public function valueAt(int $offset): mixed {} public function setValueAt(int $offset, mixed $value): void {} public function offsetGet(mixed $offset): mixed {} public function offsetExists(mixed $offset): bool {} public function offsetSet(mixed $offset, mixed $value): void {} // Throws because unset and null are different things. public function offsetUnset(mixed $offset): void {} public function indexOf(mixed $value): int|false {} public function contains(mixed $value): bool {} public function shrinkToFit(): void {} public function jsonSerialize(): array {} } ``` --- ext/spl/config.m4 | 4 +- ext/spl/config.w32 | 4 +- ext/spl/php_spl.c | 2 + ext/spl/spl_fixedarray.c | 32 +- ext/spl/spl_util.h | 37 + ext/spl/spl_vector.c | 1197 +++++++++++++++++ ext/spl/spl_vector.h | 24 + ext/spl/spl_vector.stub.php | 45 + ext/spl/spl_vector_arginfo.h | 143 ++ ext/spl/tests/Vector/Vector.phpt | 69 + ext/spl/tests/Vector/aggregate.phpt | 17 + ext/spl/tests/Vector/arrayCast.phpt | 30 + ext/spl/tests/Vector/clear.phpt | 29 + ext/spl/tests/Vector/clone.phpt | 26 + ext/spl/tests/Vector/contains.phpt | 17 + ext/spl/tests/Vector/exceptionhandler.phpt | 36 + ext/spl/tests/Vector/offsetGet.phpt | 80 ++ ext/spl/tests/Vector/push_pop.phpt | 49 + ext/spl/tests/Vector/reinit_forbidden.phpt | 29 + ext/spl/tests/Vector/serialization.phpt | 34 + ext/spl/tests/Vector/setSize.phpt | 32 + ext/spl/tests/Vector/setValueAt.phpt | 39 + ext/spl/tests/Vector/set_state.phpt | 83 ++ ext/spl/tests/Vector/shrink_capacity.phpt | 40 + ext/spl/tests/Vector/toArray.phpt | 27 + ext/spl/tests/Vector/traversable.phpt | 106 ++ .../Vector/traversable_preserve_keys.phpt | 48 + ext/spl/tests/Vector/unserialize.phpt | 16 + 28 files changed, 2260 insertions(+), 35 deletions(-) create mode 100644 ext/spl/spl_util.h create mode 100644 ext/spl/spl_vector.c create mode 100644 ext/spl/spl_vector.h create mode 100644 ext/spl/spl_vector.stub.php create mode 100644 ext/spl/spl_vector_arginfo.h create mode 100644 ext/spl/tests/Vector/Vector.phpt create mode 100644 ext/spl/tests/Vector/aggregate.phpt create mode 100644 ext/spl/tests/Vector/arrayCast.phpt create mode 100644 ext/spl/tests/Vector/clear.phpt create mode 100644 ext/spl/tests/Vector/clone.phpt create mode 100644 ext/spl/tests/Vector/contains.phpt create mode 100644 ext/spl/tests/Vector/exceptionhandler.phpt create mode 100644 ext/spl/tests/Vector/offsetGet.phpt create mode 100644 ext/spl/tests/Vector/push_pop.phpt create mode 100644 ext/spl/tests/Vector/reinit_forbidden.phpt create mode 100644 ext/spl/tests/Vector/serialization.phpt create mode 100644 ext/spl/tests/Vector/setSize.phpt create mode 100644 ext/spl/tests/Vector/setValueAt.phpt create mode 100644 ext/spl/tests/Vector/set_state.phpt create mode 100644 ext/spl/tests/Vector/shrink_capacity.phpt create mode 100644 ext/spl/tests/Vector/toArray.phpt create mode 100644 ext/spl/tests/Vector/traversable.phpt create mode 100644 ext/spl/tests/Vector/traversable_preserve_keys.phpt create mode 100644 ext/spl/tests/Vector/unserialize.phpt diff --git a/ext/spl/config.m4 b/ext/spl/config.m4 index 589e8681d70ec..5ca3afdce7941 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_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 spl_vector.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 spl_vector.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..a6af323c6c4f4 100644 --- a/ext/spl/config.w32 +++ b/ext/spl/config.w32 @@ -1,5 +1,5 @@ // vim:ft=javascript -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"); +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 spl_vector.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_vector.h"); diff --git a/ext/spl/php_spl.c b/ext/spl/php_spl.c index c6b7e02a00ce7..d233552f8e3d9 100644 --- a/ext/spl/php_spl.c +++ b/ext/spl/php_spl.c @@ -33,6 +33,7 @@ #include "spl_observer.h" #include "spl_dllist.h" #include "spl_fixedarray.h" +#include "spl_vector.h" #include "spl_heap.h" #include "zend_exceptions.h" #include "zend_interfaces.h" @@ -708,6 +709,7 @@ PHP_MINIT_FUNCTION(spl) PHP_MINIT(spl_dllist)(INIT_FUNC_ARGS_PASSTHRU); PHP_MINIT(spl_heap)(INIT_FUNC_ARGS_PASSTHRU); PHP_MINIT(spl_fixedarray)(INIT_FUNC_ARGS_PASSTHRU); + PHP_MINIT(spl_vector)(INIT_FUNC_ARGS_PASSTHRU); PHP_MINIT(spl_observer)(INIT_FUNC_ARGS_PASSTHRU); return SUCCESS; diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c index 0fd5717224bec..3044b37de559f 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; @@ -310,37 +311,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..21727a8970b68 --- /dev/null +++ b/ext/spl/spl_util.h @@ -0,0 +1,37 @@ +#ifndef SPL_UTIL +#define SPL_UTIL + +#include "zend_types.h" + +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; +} + +#endif diff --git a/ext/spl/spl_vector.c b/ext/spl/spl_vector.c new file mode 100644 index 0000000000000..b549f360c10de --- /dev/null +++ b/ext/spl/spl_vector.c @@ -0,0 +1,1197 @@ +/* + +----------------------------------------------------------------------+ + | 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 push and remove elements from the end of the list */ +#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_spl.h" +#include "spl_vector_arginfo.h" +#include "spl_vector.h" +#include "spl_exceptions.h" +#include "spl_iterators.h" +#include "spl_util.h" +#include "ext/json/php_json.h" + +#include + +/* Miscellaneous utility functions */ +static inline zval *spl_zval_copy_range(const zval *original, size_t n) { + const size_t bytes = n * sizeof(zval); + zval *copy = emalloc(bytes); + memcpy(copy, original, bytes); + return copy; +} + +static inline void spl_zval_dtor_range(zval *it, size_t n) { + for (zval *end = it + n; it < end; it++) { + zval_ptr_dtor(it); + } +} + +#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 zend_always_inline void spl_convert_zval_list_to_php_array_list(zval *return_value, zval *entries, size_t len) +{ + if (!len) { + RETURN_EMPTY_ARRAY(); + } + + 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) { + for (size_t i = 0; i < len; i++) { + Z_TRY_ADDREF_P(&entries[i]); + ZEND_HASH_FILL_ADD(&entries[i]); + } + } ZEND_HASH_FILL_END(); + + RETURN_ARR(values); +} + +/* Vector implementation details */ + +zend_object_handlers spl_handler_Vector; +zend_class_entry *spl_ce_Vector; + +/* This is a placeholder value to distinguish between empty and uninitialized Vector instances. + * Compilers require at least one element. Make this constant - reads/writes should be impossible. */ +static const zval empty_entry_list[1]; + +typedef struct _spl_vector_entries { + size_t size; + size_t capacity; + zval *entries; +} spl_vector_entries; + +typedef struct _spl_vector { + spl_vector_entries array; + zend_object std; +} spl_vector; + +/* Used by InternalIterator returned by Vector->getIterator() */ +typedef struct _spl_vector_it { + zend_object_iterator intern; + zend_long current; +} spl_vector_it; + +static void spl_vector_raise_capacity(spl_vector *intern, const zend_long new_capacity); + +static spl_vector *spl_vector_from_object(zend_object *obj) +{ + return (spl_vector*)((char*)(obj) - XtOffsetOf(spl_vector, std)); +} + +#define Z_VECTOR_P(zv) spl_vector_from_object(Z_OBJ_P((zv))) + +/* Helps enforce the invariants in debug mode: + * - if size == 0, then entries == NULL + * - if size > 0, then entries != NULL + * - size is not less than 0 + */ +static bool spl_vector_entries_empty_size(const spl_vector_entries *array) +{ + if (array->size > 0) { + ZEND_ASSERT(array->entries != empty_entry_list); + ZEND_ASSERT(array->capacity >= array->size); + return false; + } + // This vector may have reserved capacity. + return true; +} + +static bool spl_vector_entries_empty_capacity(const spl_vector_entries *array) +{ + if (array->capacity > 0) { + ZEND_ASSERT(array->entries != empty_entry_list); + return false; + } + // This vector may have reserved capacity. + return true; +} + +static bool spl_vector_entries_uninitialized(const spl_vector_entries *array) +{ + if (array->entries == NULL) { + ZEND_ASSERT(array->size == 0); + ZEND_ASSERT(array->capacity == 0); + return true; + } + ZEND_ASSERT((array->entries == empty_entry_list && array->capacity == 0) || array->capacity > 0); + return false; +} + +static void spl_vector_raise_capacity(spl_vector *intern, const zend_long new_capacity) { + ZEND_ASSERT(new_capacity > intern->array.capacity); + if (intern->array.capacity == 0) { + intern->array.entries = safe_emalloc(new_capacity, sizeof(zval), 0); + } else { + intern->array.entries = safe_erealloc(intern->array.entries, new_capacity, sizeof(zval), 0); + } + intern->array.capacity = new_capacity; + ZEND_ASSERT(intern->array.entries != NULL); +} + +static inline void spl_vector_shrink_capacity(spl_vector_entries *array, size_t size, size_t capacity, zval *old_entries) { + ZEND_ASSERT(size <= capacity); + ZEND_ASSERT(size == array->size); + ZEND_ASSERT(capacity > 0); + ZEND_ASSERT(capacity < array->capacity); + ZEND_ASSERT(old_entries == array->entries); + zval *new_entries = safe_emalloc(capacity, sizeof(zval), 0); + ZEND_ASSERT(new_entries != NULL); + memcpy(new_entries, old_entries, size * sizeof(zval)); + + array->entries = new_entries; + array->capacity = capacity; + efree(old_entries); +} + +/* Initializes the range [from, to) to null. Does not dtor existing entries. */ +/* TODO: Delete if this isn't used in the final version +static void spl_vector_entries_init_elems(spl_vector_entries *array, zend_long from, zend_long to) +{ + ZEND_ASSERT(from <= to); + zval *begin = &array->entries[from]; + zval *end = &array->entries[to]; + + while (begin != end) { + ZVAL_NULL(begin++); + } +} +*/ + +static inline void spl_vector_entries_set_empty_list(spl_vector_entries *array) { + array->size = 0; + array->capacity = 0; + array->entries = (zval *)empty_entry_list; +} + +static void spl_vector_entries_init_from_array(spl_vector_entries *array, zend_array *values, bool preserve_keys) +{ + const zend_long num_elements = zend_hash_num_elements(values); + if (num_elements <= 0) { + spl_vector_entries_set_empty_list(array); + return; + } + + zval *val; + zval *entries; + + array->size = 0; /* reset size in case emalloc() fails */ + if (preserve_keys) { + zend_string *str_index; + zend_ulong num_index, max_index = 0; + + ZEND_HASH_FOREACH_KEY(values, num_index, str_index) { + if (str_index != NULL || (zend_long)num_index < 0) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "array must contain only positive integer keys"); + return; + } + + if (num_index > max_index) { + max_index = num_index; + } + } ZEND_HASH_FOREACH_END(); + + const zend_long size = max_index + 1; + if (size <= 0) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "integer overflow detected"); + return; + } + array->entries = entries = safe_emalloc(size, sizeof(zval), 0); + array->size = size; + array->capacity = size; + /* Optimization: Don't need to null remaining elements if all elements from 0..num_elements-1 are set. */ + ZEND_ASSERT(size >= num_elements); + if (size > num_elements) { + for (zend_long i = 0; i < size; i++) { + ZVAL_NULL(&entries[i]); + } + } + + ZEND_HASH_FOREACH_KEY_VAL(values, num_index, str_index, val) { + ZEND_ASSERT(num_index < size); + ZEND_ASSERT(!str_index); + ZVAL_COPY_DEREF(&entries[num_index], val); + } ZEND_HASH_FOREACH_END(); + return; + } + + int i = 0; + array->entries = entries = safe_emalloc(num_elements, sizeof(zval), 0); + array->size = num_elements; + array->capacity = num_elements; + ZEND_HASH_FOREACH_VAL(values, val) { + ZEND_ASSERT(i < num_elements); + ZVAL_COPY_DEREF(&entries[i], val); + i++; + } ZEND_HASH_FOREACH_END(); +} + +static void spl_vector_entries_init_from_traversable(spl_vector_entries *array, zend_object *obj, bool preserve_keys) +{ + zend_class_entry *ce = obj->ce; + zend_object_iterator *iter; + zend_long size = 0, capacity = 0; + array->size = 0; + array->entries = NULL; + zval *entries = 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))) { + return; + } + } + + if (preserve_keys) { + if (!funcs->get_current_key) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Saw traversable without keys"); + return; + } + + /* size is 0 or max_index + 1 */ + while (funcs->valid(iter) == SUCCESS) { + if (UNEXPECTED(EG(exception))) { + break; + } + + zval key; + funcs->get_current_key(iter, &key); + if (UNEXPECTED(EG(exception))) { + break; + } + if (Z_TYPE(key) != IS_LONG || Z_LVAL(key) < 0) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "array must contain only positive integer keys"); + break; + } + const zend_long num_index = Z_LVAL(key); + if (num_index >= capacity) { + if (UNEXPECTED((zend_ulong) num_index > ZEND_LONG_MAX / 2 - 1)) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "integer overflow detected"); + break; + } + const zend_long new_capacity = (num_index + 1) * 2; + entries = safe_erealloc(entries, new_capacity, sizeof(zval), 0); + for ( ; capacity < new_capacity; capacity++) { + ZVAL_NULL(&entries[capacity]); + } + } + + zval *value = funcs->get_current_data(iter); + if (UNEXPECTED(EG(exception))) { + break; + } + + if (num_index >= size) { + size = num_index + 1; + ZEND_ASSERT(size <= capacity); + } + + /* Allow Traversables such as Generators to repeat keys. Silently overwrite the old key. */ + zval_ptr_dtor(&entries[num_index]); + if (UNEXPECTED(EG(exception))) { + break; + } + ZVAL_COPY_DEREF(&entries[num_index], value); + + iter->index++; + funcs->move_forward(iter); + if (UNEXPECTED(EG(exception))) { + break; + } + } + } else { + /* Reindex keys from 0. */ + while (funcs->valid(iter) == SUCCESS) { + if (EG(exception)) { + break; + } + zval *value = funcs->get_current_data(iter); + if (UNEXPECTED(EG(exception))) { + break; + } + 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 */ + if (entries) { + capacity *= 2; + entries = safe_erealloc(entries, capacity, sizeof(zval), 0); + } else { + capacity = 4; + entries = safe_emalloc(capacity, sizeof(zval), 0); + } + } + ZVAL_COPY_DEREF(&entries[size], value); + size++; + + iter->index++; + funcs->move_forward(iter); + if (UNEXPECTED(EG(exception))) { + break; + } + } + } + if (capacity > size) { + /* Shrink allocated value to actual required size */ + entries = erealloc(entries, size * sizeof(zval)); + } + + array->size = size; + array->capacity = size; + array->entries = entries; + if (iter) { + zend_iterator_dtor(iter); + } +} + +/* Copies the range [begin, end) into the vector, beginning at `offset`. + * Does not dtor the existing elements. + */ +static void spl_vector_copy_range(spl_vector_entries *array, size_t offset, zval *begin, zval *end) +{ + ZEND_ASSERT(array->size - offset >= end - begin); + + zval *to = &array->entries[offset]; + while (begin != end) { + ZVAL_COPY(to, begin); + begin++; + to++; + } +} + +static void spl_vector_entries_copy_ctor(spl_vector_entries *to, const spl_vector_entries *from) +{ + zend_long size = from->size; + if (!size) { + spl_vector_entries_set_empty_list(to); + return; + } + + to->size = 0; /* reset size in case emalloc() fails */ + to->capacity = 0; + to->entries = safe_emalloc(size, sizeof(zval), 0); + to->size = size; + to->capacity = size; + + zval *begin = from->entries, *end = from->entries + size; + spl_vector_copy_range(to, 0, begin, end); +} + +/* Destructs the entries in the range [from, to). + * Caller is expected to bounds check. + */ +static void spl_vector_entries_dtor_range(spl_vector_entries *array, size_t from, size_t to) +{ + zval *begin = array->entries + from, *end = array->entries + to; + while (begin != end) { + zval_ptr_dtor(begin); + begin++; + } +} + +/* 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 spl_vector_entries_dtor(spl_vector_entries *array) +{ + if (!spl_vector_entries_empty_capacity(array)) { + spl_vector_entries_dtor_range(array, 0, array->size); + efree(array->entries); + } +} + +static HashTable* spl_vector_get_gc(zend_object *obj, zval **table, int *n) +{ + spl_vector *intern = spl_vector_from_object(obj); + + *table = intern->array.entries; + *n = (int)intern->array.size; + + // Returning the object's properties is redundant if dynamic properties are not allowed, + // and this can't be subclassed. + return NULL; +} + +static HashTable* spl_vector_get_properties(zend_object *obj) +{ + spl_vector *intern = spl_vector_from_object(obj); + HashTable *ht = zend_std_get_properties(obj); + + /* Re-initialize properties array */ + + // Note that destructors may mutate the original array, + // so we fetch the size and circular buffer each time to avoid invalid memory accesses. + for (size_t i = 0; i < intern->array.size; i++) { + zval *elem = &intern->array.entries[i]; + Z_TRY_ADDREF_P(elem); + zend_hash_index_update(ht, i, elem); + } + const size_t properties_size = zend_hash_num_elements(ht); + if (UNEXPECTED(properties_size > intern->array.size)) { + for (size_t i = intern->array.size; i < properties_size; i++) { + zend_hash_index_del(ht, i); + } + } + + return ht; +} + +static void spl_vector_free_storage(zend_object *object) +{ + spl_vector *intern = spl_vector_from_object(object); + spl_vector_entries_dtor(&intern->array); + zend_object_std_dtor(&intern->std); +} + +static zend_object *spl_vector_new_ex(zend_class_entry *class_type, zend_object *orig, bool clone_orig) +{ + spl_vector *intern; + + intern = zend_object_alloc(sizeof(spl_vector), class_type); + /* This is a final class */ + ZEND_ASSERT(class_type == spl_ce_Vector); + + zend_object_std_init(&intern->std, class_type); + object_properties_init(&intern->std, class_type); + intern->std.handlers = &spl_handler_Vector; + + if (orig && clone_orig) { + spl_vector *other = spl_vector_from_object(orig); + spl_vector_entries_copy_ctor(&intern->array, &other->array); + } else { + intern->array.entries = NULL; + } + + return &intern->std; +} + +static zend_object *spl_vector_new(zend_class_entry *class_type) +{ + return spl_vector_new_ex(class_type, NULL, 0); +} + + +static zend_object *spl_vector_clone(zend_object *old_object) +{ + zend_object *new_object = spl_vector_new_ex(old_object->ce, old_object, 1); + + zend_objects_clone_members(new_object, old_object); + + return new_object; +} + +static int spl_vector_count_elements(zend_object *object, zend_long *count) +{ + const spl_vector *intern = spl_vector_from_object(object); + *count = intern->array.size; + return SUCCESS; +} + +/* Get number of entries in this vector */ +PHP_METHOD(Vector, count) +{ + zval *object = ZEND_THIS; + + ZEND_PARSE_PARAMETERS_NONE(); + + const spl_vector *intern = Z_VECTOR_P(object); + RETURN_LONG(intern->array.size); +} + +/* Get capacity of this vector */ +PHP_METHOD(Vector, capacity) +{ + zval *object = ZEND_THIS; + + ZEND_PARSE_PARAMETERS_NONE(); + + const spl_vector *intern = Z_VECTOR_P(object); + RETURN_LONG(intern->array.capacity); +} + +/* Free elements and backing storage of this vector */ +PHP_METHOD(Vector, clear) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + if (intern->array.capacity == 0) { + /* Nothing to clear */ + return; + } + /* Immediately make the original storage inaccessible and set count/capacity to 0 in case destructors modify the vector */ + const size_t old_size = intern->array.size; + zval *const old_entries = intern->array.entries; + spl_vector_entries_set_empty_list(&intern->array); + spl_zval_dtor_range(old_entries, old_size); + efree(old_entries); +} + +/* Set size of this vector */ +PHP_METHOD(Vector, setSize) +{ + zend_long size; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_LONG(size) + ZEND_PARSE_PARAMETERS_END(); + + if (size < 0) { + zend_argument_value_error(1, "must be greater than or equal to 0"); + RETURN_THROWS(); + } + + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t old_size = intern->array.size; + if (size > old_size) { + /* Raise the capacity as needed and fill the space with nulls */ + if (size > intern->array.capacity) { + spl_vector_raise_capacity(intern, size); + } + intern->array.size = size; + zval * const entries = intern->array.entries; + for (size_t i = old_size; i < size; i++) { + ZVAL_NULL(&entries[i]); + } + return; + } + /* Reduce the size and invalidate memory. If a destructor unexpectedly changes the size then read the new size and keep removing elements. */ + const size_t entries_to_remove = old_size - size; + if (entries_to_remove == 0) { + return; + } + zval *const old_entries = intern->array.entries; + zval * old_copy; + if (size == 0) { + old_copy = old_entries; + spl_vector_entries_set_empty_list(&intern->array); + } else { + old_copy = spl_zval_copy_range(&old_entries[size], entries_to_remove); + intern->array.size = size; + + if (size * 4 < intern->array.capacity) { + const size_t size = old_size - 1; + const size_t capacity = size > 2 ? size * 2 : 4; + if (capacity < intern->array.capacity) { + spl_vector_shrink_capacity(&intern->array, size, capacity, old_entries); + } + } + } + + spl_zval_dtor_range(old_copy, entries_to_remove); + efree(old_copy); +} + +/* Create this from an optional iterable */ +PHP_METHOD(Vector, __construct) +{ + zval *object = ZEND_THIS; + zval* iterable = NULL; + bool preserve_keys = true; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_ITERABLE(iterable) + Z_PARAM_BOOL(preserve_keys) + ZEND_PARSE_PARAMETERS_END(); + + spl_vector *intern = Z_VECTOR_P(object); + + if (UNEXPECTED(!spl_vector_entries_uninitialized(&intern->array))) { + zend_throw_exception(spl_ce_RuntimeException, "Called Vector::__construct twice", 0); + /* called __construct() twice, bail out */ + RETURN_THROWS(); + } + if (!iterable) { + spl_vector_entries_set_empty_list(&intern->array); + return; + } + + switch (Z_TYPE_P(iterable)) { + case IS_ARRAY: + spl_vector_entries_init_from_array(&intern->array, Z_ARRVAL_P(iterable), preserve_keys); + return; + case IS_OBJECT: + spl_vector_entries_init_from_traversable(&intern->array, Z_OBJ_P(iterable), preserve_keys); + return; + EMPTY_SWITCH_DEFAULT_CASE(); + } +} + +PHP_METHOD(Vector, getIterator) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + zend_create_internal_iterator_zval(return_value, ZEND_THIS); +} + +static void spl_vector_it_dtor(zend_object_iterator *iter) +{ + zval_ptr_dtor(&iter->data); +} + +static void spl_vector_it_rewind(zend_object_iterator *iter) +{ + ((spl_vector_it*)iter)->current = 0; +} + +static int spl_vector_it_valid(zend_object_iterator *iter) +{ + spl_vector_it *iterator = (spl_vector_it*)iter; + spl_vector *object = Z_VECTOR_P(&iter->data); + + if (iterator->current >= 0 && iterator->current < object->array.size) { + return SUCCESS; + } + + return FAILURE; +} + +static zval *spl_vector_read_offset_helper(spl_vector *intern, size_t offset) +{ + /* we have to return NULL on error here to avoid memleak because of + * ZE duplicating uninitialized_zval_ptr */ + if (UNEXPECTED(offset >= intern->array.size)) { + zend_throw_exception(spl_ce_OutOfBoundsException, "Index out of range", 0); + return NULL; + } else { + return &intern->array.entries[offset]; + } +} + +static zval *spl_vector_it_get_current_data(zend_object_iterator *iter) +{ + spl_vector_it *iterator = (spl_vector_it*)iter; + spl_vector *object = Z_VECTOR_P(&iter->data); + + zval *data = spl_vector_read_offset_helper(object, iterator->current); + + if (UNEXPECTED(data == NULL)) { + return &EG(uninitialized_zval); + } else { + return data; + } +} + +static void spl_vector_it_get_current_key(zend_object_iterator *iter, zval *key) +{ + spl_vector_it *iterator = (spl_vector_it*)iter; + spl_vector *object = Z_VECTOR_P(&iter->data); + + size_t offset = iterator->current; + if (offset >= object->array.size) { + ZVAL_NULL(key); + } else { + ZVAL_LONG(key, offset); + } +} + +static void spl_vector_it_move_forward(zend_object_iterator *iter) +{ + ((spl_vector_it*)iter)->current++; +} + +/* iterator handler table */ +static const zend_object_iterator_funcs spl_vector_it_funcs = { + spl_vector_it_dtor, + spl_vector_it_valid, + spl_vector_it_get_current_data, + spl_vector_it_get_current_key, + spl_vector_it_move_forward, + spl_vector_it_rewind, + NULL, + NULL, /* get_gc */ +}; + + +zend_object_iterator *spl_vector_get_iterator(zend_class_entry *ce, zval *object, int by_ref) +{ + // This is final + ZEND_ASSERT(ce == spl_ce_Vector); + spl_vector_it *iterator; + + if (UNEXPECTED(by_ref)) { + zend_throw_error(NULL, "An iterator cannot be used with foreach by reference"); + return NULL; + } + + iterator = emalloc(sizeof(spl_vector_it)); + + zend_iterator_init((zend_object_iterator*)iterator); + + ZVAL_OBJ_COPY(&iterator->intern.data, Z_OBJ_P(object)); + iterator->intern.funcs = &spl_vector_it_funcs; + + return &iterator->intern; +} + +PHP_METHOD(Vector, __unserialize) +{ + HashTable *raw_data; + zval *val; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &raw_data) == FAILURE) { + RETURN_THROWS(); + } + + const size_t num_entries = zend_hash_num_elements(raw_data); + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + if (UNEXPECTED(!spl_vector_entries_uninitialized(&intern->array))) { + zend_throw_exception(spl_ce_RuntimeException, "Already unserialized", 0); + RETURN_THROWS(); + } + + ZEND_ASSERT(intern->array.entries == NULL); + + zval *const entries = safe_emalloc(num_entries, sizeof(zval), 0); + zval *it = entries; + + zend_string *str; + + ZEND_HASH_FOREACH_STR_KEY_VAL(raw_data, str, val) { + if (UNEXPECTED(str)) { + for (zval *deleteIt = entries; deleteIt < it; deleteIt++) { + zval_ptr_dtor_nogc(deleteIt); + } + efree(entries); + zend_throw_exception(spl_ce_UnexpectedValueException, "Vector::__unserialize saw unexpected string key, expected sequence of values", 0); + RETURN_THROWS(); + } + ZVAL_COPY_DEREF(it++, val); + } ZEND_HASH_FOREACH_END(); + ZEND_ASSERT(it == entries + num_entries); + + intern->array.size = num_entries; + intern->array.capacity = num_entries; + intern->array.entries = entries; +} + +static void spl_vector_entries_init_from_array_values(spl_vector_entries *array, zend_array *raw_data) +{ + size_t num_entries = zend_hash_num_elements(raw_data); + if (num_entries == 0) { + array->size = 0; + array->capacity = 0; + array->entries = NULL; + return; + } + zval *entries = safe_emalloc(num_entries, sizeof(zval), 0); + size_t actual_size = 0; + zval *val; + ZEND_HASH_FOREACH_VAL(raw_data, val) { + ZVAL_COPY_DEREF(&entries[actual_size], val); + actual_size++; + } ZEND_HASH_FOREACH_END(); + + ZEND_ASSERT(actual_size <= num_entries); + if (UNEXPECTED(!actual_size)) { + efree(entries); + entries = NULL; + num_entries = 0; + } + + array->entries = entries; + array->size = actual_size; + array->capacity = num_entries; +} + +PHP_METHOD(Vector, __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 = spl_vector_new(spl_ce_Vector); + spl_vector *intern = spl_vector_from_object(object); + spl_vector_entries_init_from_array_values(&intern->array, array_ht); + + RETURN_OBJ(object); +} + +PHP_METHOD(Vector, __serialize) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + + if (spl_vector_entries_empty_size(&intern->array)) { + RETURN_EMPTY_ARRAY(); + } + zval *entries = intern->array.entries; + size_t len = intern->array.size; + zend_array *flat_entries_array = zend_new_array(len * 2); + /* Initialize return array */ + zend_hash_real_init_packed(flat_entries_array); + + /* Go through entries and add keys and values to the return array */ + ZEND_HASH_FILL_PACKED(flat_entries_array) { + for (size_t i = 0; i < len; i++) { + zval *tmp = &entries[i]; + Z_TRY_ADDREF_P(tmp); + ZEND_HASH_FILL_ADD(tmp); + } + } ZEND_HASH_FILL_END(); + + RETURN_ARR(flat_entries_array); +} + +PHP_METHOD(Vector, toArray) +{ + ZEND_PARSE_PARAMETERS_NONE(); + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + size_t len = intern->array.size; + if (!len) { + RETURN_EMPTY_ARRAY(); + } + zval *entries = intern->array.entries; + 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) { + for (size_t i = 0; i < len; i++) { + zval *tmp = &entries[i]; + Z_TRY_ADDREF_P(tmp); + ZEND_HASH_FILL_ADD(tmp); + } + } ZEND_HASH_FILL_END(); + RETURN_ARR(values); +} + +static zend_always_inline void spl_vector_get_value_at_offset(zval *return_value, const zval *zval_this, zend_long offset) +{ + spl_vector *intern = Z_VECTOR_P(zval_this); + size_t len = intern->array.size; + if (UNEXPECTED((zend_ulong) offset >= len)) { + zend_throw_exception(spl_ce_OutOfBoundsException, "Index out of range", 0); + RETURN_THROWS(); + } + RETURN_COPY(&intern->array.entries[offset]); +} + +PHP_METHOD(Vector, valueAt) +{ + zend_long offset; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + spl_vector_get_value_at_offset(return_value, ZEND_THIS, offset); +} + +PHP_METHOD(Vector, offsetGet) +{ + 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); + + spl_vector_get_value_at_offset(return_value, ZEND_THIS, offset); +} + +PHP_METHOD(Vector, 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 spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t len = intern->array.size; + RETURN_BOOL((zend_ulong) offset < len); +} + +PHP_METHOD(Vector, indexOf) +{ + zval *value; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + const spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t len = intern->array.size; + zval *entries = intern->array.entries; + for (size_t i = 0; i < len; i++) { + if (zend_is_identical(value, &entries[i])) { + RETURN_LONG(i); + } + } + RETURN_FALSE; +} + +PHP_METHOD(Vector, contains) +{ + zval *value; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + const spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t len = intern->array.size; + zval *entries = intern->array.entries; + for (size_t i = 0; i < len; i++) { + if (zend_is_identical(value, &entries[i])) { + RETURN_TRUE; + } + } + RETURN_FALSE; +} + +static zend_always_inline void spl_vector_set_value_at_offset(zend_object *object, zend_long offset, zval *value) { + const spl_vector *intern = spl_vector_from_object(object); + zval *const ptr = &intern->array.entries[offset]; + + size_t len = intern->array.size; + if (UNEXPECTED((zend_ulong) offset >= len)) { + zend_throw_exception(spl_ce_OutOfBoundsException, "Index out of range", 0); + return; + } + zval tmp; + ZVAL_COPY_VALUE(&tmp, ptr); + ZVAL_COPY(ptr, value); + zval_ptr_dtor(&tmp); +} + +PHP_METHOD(Vector, setValueAt) +{ + zend_long offset; + zval *value; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_LONG(offset) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + spl_vector_set_value_at_offset(Z_OBJ_P(ZEND_THIS), offset, value); +} + +PHP_METHOD(Vector, 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); + + spl_vector_set_value_at_offset(Z_OBJ_P(ZEND_THIS), offset, value); +} + +PHP_METHOD(Vector, push) +{ + zval *value; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t old_size = intern->array.size; + const size_t old_capacity = intern->array.capacity; + + if (old_size >= old_capacity) { + ZEND_ASSERT(old_size == old_capacity); + spl_vector_raise_capacity(intern, old_size ? old_size * 2 : 4); + } + ZVAL_COPY(&intern->array.entries[old_size], value); + intern->array.size++; +} + +PHP_METHOD(Vector, pop) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t old_size = intern->array.size; + if (old_size == 0) { + zend_throw_exception(spl_ce_UnderflowException, "Cannot pop from empty Vector", 0); + RETURN_THROWS(); + } + const size_t old_capacity = intern->array.capacity; + intern->array.size--; + RETVAL_COPY_VALUE(&intern->array.entries[intern->array.size]); + if (old_size * 4 < old_capacity) { + /* Shrink the storage if only a quarter of the capacity is used */ + const size_t size = old_size - 1; + zval *old_entries = intern->array.entries; + const size_t capacity = size > 2 ? size * 2 : 4; + if (capacity < old_capacity) { + spl_vector_shrink_capacity(&intern->array, size, capacity, old_entries); + } + } +} + +PHP_METHOD(Vector, shrinkToFit) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t size = intern->array.size; + const size_t old_capacity = intern->array.capacity; + if (size >= old_capacity) { + ZEND_ASSERT(size == old_capacity); + return; + } + + if (size == 0) { + efree(intern->array.entries); + intern->array.entries = (zval *)empty_entry_list; + } else { + intern->array.entries = safe_erealloc(intern->array.entries, size, sizeof(zval), 0); + } + intern->array.capacity = size; +} + +PHP_METHOD(Vector, offsetUnset) +{ + zval *offset_zv; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &offset_zv) == FAILURE) { + RETURN_THROWS(); + } + zend_throw_exception(spl_ce_RuntimeException, "Vector does not support offsetUnset - elements must be set to null or removed by resizing", 0); + RETURN_THROWS(); +} + +static void spl_vector_return_list(zval *return_value, spl_vector *intern) +{ + spl_convert_zval_list_to_php_array_list(return_value, intern->array.entries, intern->array.size); +} + +PHP_METHOD(Vector, jsonSerialize) +{ + /* json_encoder.c will always encode objects as {"0":..., "1":...}, and detects recursion if an object returns its internal property array, so we have to return a new array */ + ZEND_PARSE_PARAMETERS_NONE(); + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + spl_vector_return_list(return_value, intern); +} + +static void spl_vector_write_dimension(zend_object *object, zval *offset_zv, zval *value) +{ + if (!offset_zv) { + zend_throw_exception(spl_ce_RuntimeException, "[] operator not supported for Vector", 0); + return; + } + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); + + const spl_vector *intern = spl_vector_from_object(object); + if (offset < 0 || offset >= intern->array.size) { + zend_throw_exception(spl_ce_OutOfBoundsException, "Index invalid or out of range", 0); + return; + } + spl_vector_set_value_at_offset(object, offset, value); +} + +static zval *spl_vector_read_dimension(zend_object *object, zval *offset_zv, int type, zval *rv) +{ + if (!offset_zv) { + zend_throw_exception(spl_ce_RuntimeException, "[] operator not supported for Vector", 0); + return NULL; + } + + zend_long offset; + CONVERT_OFFSET_TO_LONG_OR_THROW_RETURN_NULLPTR(offset, offset_zv); + + const spl_vector *intern = spl_vector_from_object(object); + + if (offset < 0 || offset >= intern->array.size) { + if (type != BP_VAR_IS) { + zend_throw_exception(spl_ce_OutOfBoundsException, "Index out of range", 0); + } + return NULL; + } else { + return &intern->array.entries[offset]; + } +} + +PHP_MINIT_FUNCTION(spl_vector) +{ + spl_ce_Vector = register_class_Vector(zend_ce_aggregate, zend_ce_countable, php_json_serializable_ce, zend_ce_arrayaccess); + spl_ce_Vector->create_object = spl_vector_new; + + memcpy(&spl_handler_Vector, &std_object_handlers, sizeof(zend_object_handlers)); + + spl_handler_Vector.offset = XtOffsetOf(spl_vector, std); + spl_handler_Vector.clone_obj = spl_vector_clone; + spl_handler_Vector.count_elements = spl_vector_count_elements; + spl_handler_Vector.get_properties = spl_vector_get_properties; + spl_handler_Vector.get_gc = spl_vector_get_gc; + spl_handler_Vector.dtor_obj = zend_objects_destroy_object; + spl_handler_Vector.free_obj = spl_vector_free_storage; + + spl_handler_Vector.read_dimension = spl_vector_read_dimension; + spl_handler_Vector.write_dimension = spl_vector_write_dimension; + + spl_ce_Vector->ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; + spl_ce_Vector->get_iterator = spl_vector_get_iterator; + + return SUCCESS; +} diff --git a/ext/spl/spl_vector.h b/ext/spl/spl_vector.h new file mode 100644 index 0000000000000..d7807105a784e --- /dev/null +++ b/ext/spl/spl_vector.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 SPL_VECTOR_H +#define SPL_VECTOR_H + +extern PHPAPI zend_class_entry *spl_ce_Vector; + +PHP_MINIT_FUNCTION(spl_vector); + +#endif /* SPL_VECTOR_H */ diff --git a/ext/spl/spl_vector.stub.php b/ext/spl/spl_vector.stub.php new file mode 100644 index 0000000000000..7067a594b2287 --- /dev/null +++ b/ext/spl/spl_vector.stub.php @@ -0,0 +1,45 @@ +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/spl/tests/Vector/Vector.phpt b/ext/spl/tests/Vector/Vector.phpt new file mode 100644 index 0000000000000..39566c8b27227 --- /dev/null +++ b/ext/spl/tests/Vector/Vector.phpt @@ -0,0 +1,69 @@ +--TEST-- +Vector constructed from array +--FILE-- + 'x', 'second' => new stdClass()], preserveKeys: false); +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 Vector([]); +var_dump($it); +var_dump((array)$it); +foreach ($it as $key => $value) { + echo "Unreachable\n"; +} + +// The default is to preserve keys +$it = new Vector([2 => 'third', 0 => 'first']); +var_dump($it); + +try { + $it = new Vector([-1 => new stdClass()]); +} catch (UnexpectedValueException $e) { + echo "Caught: {$e->getMessage()}\n"; +} + +try { + $it = new Vector(['0a' => new stdClass()]); +} catch (UnexpectedValueException $e) { + echo "Caught: {$e->getMessage()}\n"; +} +?> +--EXPECT-- +Key: 0 +Value: 'x' +Key: 1 +Value: (object) array( +) +object(Vector)#1 (2) { + [0]=> + string(1) "x" + [1]=> + object(stdClass)#2 (0) { + } +} +array(2) { + [0]=> + string(1) "x" + [1]=> + object(stdClass)#2 (0) { + } +} +object(Vector)#3 (0) { +} +array(0) { +} +object(Vector)#1 (3) { + [0]=> + string(5) "first" + [1]=> + NULL + [2]=> + string(5) "third" +} +Caught: array must contain only positive integer keys +Caught: array must contain only positive integer keys \ No newline at end of file diff --git a/ext/spl/tests/Vector/aggregate.phpt b/ext/spl/tests/Vector/aggregate.phpt new file mode 100644 index 0000000000000..942c1d210974f --- /dev/null +++ b/ext/spl/tests/Vector/aggregate.phpt @@ -0,0 +1,17 @@ +--TEST-- +Vector is an IteratorAggregate +--FILE-- + '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/spl/tests/Vector/arrayCast.phpt b/ext/spl/tests/Vector/arrayCast.phpt new file mode 100644 index 0000000000000..befd62e298dd0 --- /dev/null +++ b/ext/spl/tests/Vector/arrayCast.phpt @@ -0,0 +1,30 @@ +--TEST-- +Vector to array +--FILE-- +pop(); +var_dump($it); + + +?> +--EXPECT-- +array(1) { + [0]=> + string(4) "TEST" +} +array(1) { + [0]=> + string(5) "TEST2" +} +object(Vector)#1 (1) { + [0]=> + string(5) "TEST2" +} +object(Vector)#1 (0) { +} diff --git a/ext/spl/tests/Vector/clear.phpt b/ext/spl/tests/Vector/clear.phpt new file mode 100644 index 0000000000000..82e0141b7dd4f --- /dev/null +++ b/ext/spl/tests/Vector/clear.phpt @@ -0,0 +1,29 @@ +--TEST-- +Vector clear +--FILE-- +toArray()); +var_dump($it->count()); +var_dump($it->capacity()); +$it->clear(); +foreach ($it as $value) { + echo "Not reached\n"; +} +var_dump($it->toArray()); +var_dump($it->count()); +var_dump($it->capacity()); +?> +--EXPECT-- +array(1) { + [0]=> + object(stdClass)#2 (0) { + } +} +int(1) +int(1) +array(0) { +} +int(0) +int(0) diff --git a/ext/spl/tests/Vector/clone.phpt b/ext/spl/tests/Vector/clone.phpt new file mode 100644 index 0000000000000..35a215849e072 --- /dev/null +++ b/ext/spl/tests/Vector/clone.phpt @@ -0,0 +1,26 @@ +--TEST-- +Vector can be cloned +--FILE-- + $value) { + echo "Saw entry:\n"; + var_dump($key, $value); +} + +?> +--EXPECT-- +Saw entry: +int(0) +object(stdClass)#2 (0) { +} +Saw entry: +int(1) +object(ArrayObject)#3 (1) { + ["storage":"ArrayObject":private]=> + array(0) { + } +} \ No newline at end of file diff --git a/ext/spl/tests/Vector/contains.phpt b/ext/spl/tests/Vector/contains.phpt new file mode 100644 index 0000000000000..1660af21ef450 --- /dev/null +++ b/ext/spl/tests/Vector/contains.phpt @@ -0,0 +1,17 @@ +--TEST-- +Vector contains()/indexOf(); +--FILE-- +contains($value)), json_encode($it->indexOf($value))); +} +?> +--EXPECT-- +null: contains=false, indexOf=false +"o": contains=false, indexOf=false +{}: contains=true, indexOf=1 +{}: contains=false, indexOf=false +"first": contains=true, indexOf=0 diff --git a/ext/spl/tests/Vector/exceptionhandler.phpt b/ext/spl/tests/Vector/exceptionhandler.phpt new file mode 100644 index 0000000000000..b6fc1ff11fa3b --- /dev/null +++ b/ext/spl/tests/Vector/exceptionhandler.phpt @@ -0,0 +1,36 @@ +--TEST-- +Vector 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 Vector(yields_and_throws(), preserveKeys: false); +} 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/spl/tests/Vector/offsetGet.phpt b/ext/spl/tests/Vector/offsetGet.phpt new file mode 100644 index 0000000000000..fec7da758e2ce --- /dev/null +++ b/ext/spl/tests/Vector/offsetGet.phpt @@ -0,0 +1,80 @@ +--TEST-- +Vector offsetGet/valueAt +--FILE-- +getMessage()); + } +} +expect_throws(fn() => (new ReflectionClass(Vector::class))->newInstanceWithoutConstructor()); +$it = new Vector([new stdClass()]); +var_dump($it->offsetGet(0)); +var_dump($it->valueAt(0)); +expect_throws(fn() => $it->offsetSet(1,'x')); +expect_throws(fn() => $it->offsetUnset(0)); +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->valueAt(1)); +expect_throws(fn() => $it->valueAt(-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->valueAt(PHP_INT_MAX)); +expect_throws(fn() => $it->valueAt(PHP_INT_MIN)); +expect_throws(fn() => $it->valueAt(1)); +expect_throws(fn() => $it->valueAt(-1)); +expect_throws(fn() => $it->offsetGet(1)); +expect_throws(fn() => $it->offsetGet('1')); +expect_throws(fn() => $it->offsetGet('invalid')); +expect_throws(fn() => $it->valueAt('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[0]); }); +var_dump($it->getIterator()); +?> +--EXPECT-- +Caught ReflectionException: Class Vector is an internal class marked as final that cannot be instantiated without invoking its constructor +object(stdClass)#1 (0) { +} +object(stdClass)#1 (0) { +} +Caught OutOfBoundsException: Index out of range +Caught RuntimeException: Vector does not support offsetUnset - elements must be set to null or removed by resizing +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 OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +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: Vector::valueAt(): Argument #1 ($offset) must be of type int, string given +Caught TypeError: Illegal offset type +Caught RuntimeException: Vector does not support offsetUnset - elements must be set to null or removed by resizing +Caught OutOfBoundsException: Index out of range +Caught RuntimeException: Vector does not support offsetUnset - elements must be set to null or removed by resizing +object(InternalIterator)#2 (0) { +} \ No newline at end of file diff --git a/ext/spl/tests/Vector/push_pop.phpt b/ext/spl/tests/Vector/push_pop.phpt new file mode 100644 index 0000000000000..d8c3699d13ca4 --- /dev/null +++ b/ext/spl/tests/Vector/push_pop.phpt @@ -0,0 +1,49 @@ +--TEST-- +Vector push/pop +--FILE-- +getMessage()); + } +} + +echo "Test empty vector\n"; +$it = new Vector([]); +printf("count=%d capacity=%d\n", $it->count(), $it->capacity()); +expect_throws(fn() => $it->pop()); +expect_throws(fn() => $it->pop()); +$it->push(strtoupper('test')); +$it->push(['literal']); +$it->push(new stdClass()); +echo json_encode($it), "\n"; +printf("count=%d capacity=%d\n", count($it), $it->capacity()); +var_dump($it->pop()); +var_dump($it->pop()); +echo "After popping 2 elements: ", json_encode($it->toArray()), "\n"; +var_dump($it->pop()); +echo json_encode($it), "\n"; +printf("count=%d capacity=%d\n", count($it), $it->capacity()); + +?> +--EXPECT-- +Test empty vector +count=0 capacity=0 +Caught UnderflowException: Cannot pop from empty Vector +Caught UnderflowException: Cannot pop from empty Vector +["TEST",["literal"],{}] +count=3 capacity=4 +object(stdClass)#2 (0) { +} +array(1) { + [0]=> + string(7) "literal" +} +After popping 2 elements: ["TEST"] +string(4) "TEST" +[] +count=0 capacity=4 \ No newline at end of file diff --git a/ext/spl/tests/Vector/reinit_forbidden.phpt b/ext/spl/tests/Vector/reinit_forbidden.phpt new file mode 100644 index 0000000000000..df82a4389058a --- /dev/null +++ b/ext/spl/tests/Vector/reinit_forbidden.phpt @@ -0,0 +1,29 @@ +--TEST-- +Vector 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 Vector::__construct twice +object(Vector)#1 (0) { +} +Caught RuntimeException: Already unserialized +object(Vector)#1 (0) { +} \ No newline at end of file diff --git a/ext/spl/tests/Vector/serialization.phpt b/ext/spl/tests/Vector/serialization.phpt new file mode 100644 index 0000000000000..de107f1cbd026 --- /dev/null +++ b/ext/spl/tests/Vector/serialization.phpt @@ -0,0 +1,34 @@ +--TEST-- +Vector can be serialized and unserialized +--FILE-- + new stdClass()], preserveKeys: false); +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 Vector([]); +var_dump($it->__serialize()); +?> +--EXPECT-- +Caught Error: Cannot create dynamic property Vector::$dynamicProp +O:6:"Vector":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/spl/tests/Vector/setSize.phpt b/ext/spl/tests/Vector/setSize.phpt new file mode 100644 index 0000000000000..b0f8f15f2d90f --- /dev/null +++ b/ext/spl/tests/Vector/setSize.phpt @@ -0,0 +1,32 @@ +--TEST-- +Vector setSize +--FILE-- +count(), $it->capacity()); + var_dump($it->toArray()); +} + +$it = new Vector(); +show($it); +$it->setSize(2); +$it[0] = new stdClass(); +show($it); +$it->setSize(0); +show($it); +?> +--EXPECT-- +count=0 capacity=0 +array(0) { +} +count=2 capacity=2 +array(2) { + [0]=> + object(stdClass)#2 (0) { + } + [1]=> + NULL +} +count=0 capacity=0 +array(0) { +} diff --git a/ext/spl/tests/Vector/setValueAt.phpt b/ext/spl/tests/Vector/setValueAt.phpt new file mode 100644 index 0000000000000..e2bf69afb759b --- /dev/null +++ b/ext/spl/tests/Vector/setValueAt.phpt @@ -0,0 +1,39 @@ +--TEST-- +Vector offsetSet/setValueAt +--FILE-- +getMessage()); + } +} + +echo "Test empty vector\n"; +$it = new Vector([]); +expect_throws(fn() => $it->offsetSet(0, strtoupper('value'))); +expect_throws(fn() => $it->setValueAt(0, strtoupper('value'))); + +echo "Test short vector\n"; +$str = 'Test short vector'; +$it = new Vector(explode(' ', $str)); +$it->setValueAt(0, 'new'); +$it->offsetSet(2, strtoupper('test')); +echo json_encode($it), "\n"; +expect_throws(fn() => $it->setValueAt(-1, strtoupper('value'))); +expect_throws(fn() => $it->setValueAt(3, 'end')); +expect_throws(fn() => $it->setValueAt(PHP_INT_MAX, 'end')); + +?> +--EXPECT-- +Test empty vector +Caught OutOfBoundsException: Index out of range +Caught OutOfBoundsException: Index out of range +Test short vector +["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/spl/tests/Vector/set_state.phpt b/ext/spl/tests/Vector/set_state.phpt new file mode 100644 index 0000000000000..727067ce3a43f --- /dev/null +++ b/ext/spl/tests/Vector/set_state.phpt @@ -0,0 +1,83 @@ +--TEST-- +Vector::__set_state +--FILE-- + 'x']); +$it = Vector::__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-- +Vector::__set_state(array( +)) +key=0 value="A LITERAL" +key=1 value=["first","x"] +key=2 value=[{"key":"value"},null] +Vector::__set_state(array( + 0 => 'A LITERAL', + 1 => + array ( + 0 => 'first', + 1 => 'x', + ), + 2 => + array ( + 0 => + (object) array( + 'key' => 'value', + ), + 1 => NULL, + ), +)) +object(Vector)#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/spl/tests/Vector/shrink_capacity.phpt b/ext/spl/tests/Vector/shrink_capacity.phpt new file mode 100644 index 0000000000000..c7c1e78c13c53 --- /dev/null +++ b/ext/spl/tests/Vector/shrink_capacity.phpt @@ -0,0 +1,40 @@ +--TEST-- +Vector pop() shrinks capacity +--FILE-- + 0) { + var_dump($it->pop()); + printf("capacity=%d\n", $it->capacity()); +} +$it->shrinkToFit(); +printf("shrinkToFit capacity=%d\n", $it->capacity()); +printf("shrinkToFit capacity=%d\n", $it->capacity()); +$it->push(new stdClass()); +printf("it=%s count=%d capacity=%d\n", json_encode($it), count($it), $it->capacity()); +$it->shrinkToFit(); +printf("it=%s count=%d capacity=%d\n", json_encode($it), count($it), $it->capacity()); +?> +--EXPECT-- +string(4) "test" +capacity=8 +string(4) "test" +capacity=8 +string(4) "test" +capacity=8 +string(4) "test" +capacity=8 +string(4) "test" +capacity=8 +string(4) "test" +capacity=8 +string(3) "123" +capacity=8 +string(4) "test" +capacity=4 +shrinkToFit capacity=0 +shrinkToFit capacity=0 +it=[{}] count=1 capacity=4 +it=[{}] count=1 capacity=1 diff --git a/ext/spl/tests/Vector/toArray.phpt b/ext/spl/tests/Vector/toArray.phpt new file mode 100644 index 0000000000000..47a4af3cfea10 --- /dev/null +++ b/ext/spl/tests/Vector/toArray.phpt @@ -0,0 +1,27 @@ +--TEST-- +Vector toArray() +--FILE-- + new stdClass()], false); +var_dump($it->toArray()); +var_dump($it->toArray()); +$it = new Vector([]); +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/spl/tests/Vector/traversable.phpt b/ext/spl/tests/Vector/traversable.phpt new file mode 100644 index 0000000000000..c516905babd0b --- /dev/null +++ b/ext/spl/tests/Vector/traversable.phpt @@ -0,0 +1,106 @@ +--TEST-- +Vector 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"; +} + +// Vector eagerly evaluates the passed in Traversable +$it = new Vector(yields_values(), preserveKeys: false); +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); + +foreach ([false, true] as $preserveKeys) { + $emptyIt = new Vector(new ArrayObject(), $preserveKeys); + var_dump($emptyIt); + foreach ($emptyIt as $key => $value) { + echo "Unreachable\n"; + } + foreach ($emptyIt as $key => $value) { + echo "Unreachable\n"; + } + echo "Done\n"; + unset($emptyIt); +} + +?> +--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(Vector)#1 (0) { +} +Done +object(Vector)#1 (0) { +} +Done \ No newline at end of file diff --git a/ext/spl/tests/Vector/traversable_preserve_keys.phpt b/ext/spl/tests/Vector/traversable_preserve_keys.phpt new file mode 100644 index 0000000000000..e6dac37ef082e --- /dev/null +++ b/ext/spl/tests/Vector/traversable_preserve_keys.phpt @@ -0,0 +1,48 @@ +--TEST-- +Vector constructed from Traversable preserves keys +--FILE-- + (object)['key' => 'value']; + yield 0 => new stdClass();; + yield 0 => true; +} + +function yields_bad_value() { + yield 5 => (object)['key' => 'value']; + yield -1 => new stdClass(); +} + +// Vector eagerly evaluates the passed in Traversable +$it = new Vector(yields_values()); +foreach ($it as $key => $value) { + printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); +} +printf("count=%d capacity=%d\n", $it->count(), $it->capacity()); +unset($it); + +try { + $it = new Vector(yields_bad_value()); +} catch (Exception $e) { + printf("Caught %s: %s\n", get_class($e), $e->getMessage()); +} + +?> +--EXPECT-- +Key: 0 +Value: true +Key: 1 +Value: NULL +Key: 2 +Value: NULL +Key: 3 +Value: NULL +Key: 4 +Value: NULL +Key: 5 +Value: (object) array( + 'key' => 'value', +) +count=6 capacity=6 +Caught UnexpectedValueException: array must contain only positive integer keys \ No newline at end of file diff --git a/ext/spl/tests/Vector/unserialize.phpt b/ext/spl/tests/Vector/unserialize.phpt new file mode 100644 index 0000000000000..b5bac18343ea4 --- /dev/null +++ b/ext/spl/tests/Vector/unserialize.phpt @@ -0,0 +1,16 @@ +--TEST-- +Vector unserialize error handling +--FILE-- +getMessage()); + } +}); +?> +--EXPECT-- +Caught UnexpectedValueException: Vector::__unserialize saw unexpected string key, expected sequence of values From 460b1eb2831232da97973fd2ceb7343064a1cd6b Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Sun, 12 Sep 2021 13:56:53 -0400 Subject: [PATCH 2/7] Support `$vector[] = $value;` --- ext/spl/spl_vector.c | 58 ++++++++++++++++++++++-------- ext/spl/tests/Vector/push_pop.phpt | 23 +++++++++--- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/ext/spl/spl_vector.c b/ext/spl/spl_vector.c index b549f360c10de..41235f241112e 100644 --- a/ext/spl/spl_vector.c +++ b/ext/spl/spl_vector.c @@ -1043,15 +1043,8 @@ PHP_METHOD(Vector, offsetSet) spl_vector_set_value_at_offset(Z_OBJ_P(ZEND_THIS), offset, value); } -PHP_METHOD(Vector, push) +static zend_always_inline void spl_vector_push(spl_vector *intern, zval *value) { - zval *value; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_ZVAL(value) - ZEND_PARSE_PARAMETERS_END(); - - spl_vector *intern = Z_VECTOR_P(ZEND_THIS); const size_t old_size = intern->array.size; const size_t old_capacity = intern->array.capacity; @@ -1063,6 +1056,17 @@ PHP_METHOD(Vector, push) intern->array.size++; } +PHP_METHOD(Vector, push) +{ + zval *value; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + spl_vector_push(Z_VECTOR_P(ZEND_THIS), value); +} + PHP_METHOD(Vector, pop) { ZEND_PARSE_PARAMETERS_NONE(); @@ -1134,27 +1138,27 @@ PHP_METHOD(Vector, jsonSerialize) static void spl_vector_write_dimension(zend_object *object, zval *offset_zv, zval *value) { + spl_vector *intern = spl_vector_from_object(object); if (!offset_zv) { - zend_throw_exception(spl_ce_RuntimeException, "[] operator not supported for Vector", 0); + spl_vector_push(intern, value); return; } zend_long offset; CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); - const spl_vector *intern = spl_vector_from_object(object); if (offset < 0 || offset >= intern->array.size) { zend_throw_exception(spl_ce_OutOfBoundsException, "Index invalid or out of range", 0); return; } + ZVAL_DEREF(value); spl_vector_set_value_at_offset(object, offset, value); } static zval *spl_vector_read_dimension(zend_object *object, zval *offset_zv, int type, zval *rv) { - if (!offset_zv) { - zend_throw_exception(spl_ce_RuntimeException, "[] operator not supported for Vector", 0); - return NULL; + if (UNEXPECTED(!offset_zv || Z_ISUNDEF_P(offset_zv))) { + return &EG(uninitialized_zval); } zend_long offset; @@ -1162,7 +1166,7 @@ static zval *spl_vector_read_dimension(zend_object *object, zval *offset_zv, int const spl_vector *intern = spl_vector_from_object(object); - if (offset < 0 || offset >= intern->array.size) { + if (UNEXPECTED(offset < 0 || offset >= intern->array.size)) { if (type != BP_VAR_IS) { zend_throw_exception(spl_ce_OutOfBoundsException, "Index out of range", 0); } @@ -1172,6 +1176,31 @@ static zval *spl_vector_read_dimension(zend_object *object, zval *offset_zv, int } } +static int spl_vector_has_dimension(zend_object *object, zval *offset_zv, int check_empty) +{ + zend_long offset; + if (UNEXPECTED(Z_TYPE_P(offset_zv) != IS_LONG)) { + offset = spl_offset_convert_to_long(offset_zv); + if (UNEXPECTED(EG(exception))) { + return 0; + } + } else { + offset = Z_LVAL_P(offset_zv); + } + + const spl_vector *intern = spl_vector_from_object(object); + + if (UNEXPECTED(offset < 0 || offset >= intern->array.size)) { + return 0; + } + + zval *val = &intern->array.entries[offset]; + if (check_empty) { + return zend_is_true(val); + } + return Z_TYPE_P(val) != IS_NULL; +} + PHP_MINIT_FUNCTION(spl_vector) { spl_ce_Vector = register_class_Vector(zend_ce_aggregate, zend_ce_countable, php_json_serializable_ce, zend_ce_arrayaccess); @@ -1189,6 +1218,7 @@ PHP_MINIT_FUNCTION(spl_vector) spl_handler_Vector.read_dimension = spl_vector_read_dimension; spl_handler_Vector.write_dimension = spl_vector_write_dimension; + spl_handler_Vector.has_dimension = spl_vector_has_dimension; spl_ce_Vector->ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES; spl_ce_Vector->get_iterator = spl_vector_get_iterator; diff --git a/ext/spl/tests/Vector/push_pop.phpt b/ext/spl/tests/Vector/push_pop.phpt index d8c3699d13ca4..9196d56d69806 100644 --- a/ext/spl/tests/Vector/push_pop.phpt +++ b/ext/spl/tests/Vector/push_pop.phpt @@ -20,14 +20,22 @@ expect_throws(fn() => $it->pop()); $it->push(strtoupper('test')); $it->push(['literal']); $it->push(new stdClass()); +$it[] = strtoupper('test2'); +$it[] = null; echo json_encode($it), "\n"; printf("count=%d capacity=%d\n", count($it), $it->capacity()); var_dump($it->pop()); var_dump($it->pop()); -echo "After popping 2 elements: ", json_encode($it->toArray()), "\n"; +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 capacity=%d\n", count($it), $it->capacity()); +echo "After shrinkToFit\n"; +$it->shrinkToFit(); +echo json_encode($it), "\n"; +printf("count=%d capacity=%d\n", count($it), $it->capacity()); ?> --EXPECT-- @@ -35,15 +43,20 @@ Test empty vector count=0 capacity=0 Caught UnderflowException: Cannot pop from empty Vector Caught UnderflowException: Cannot pop from empty Vector -["TEST",["literal"],{}] -count=3 capacity=4 +["TEST",["literal"],{},"TEST2",null] +count=5 capacity=8 +NULL +string(5) "TEST2" object(stdClass)#2 (0) { } array(1) { [0]=> string(7) "literal" } -After popping 2 elements: ["TEST"] +After popping 4 elements: ["TEST"] string(4) "TEST" [] -count=0 capacity=4 \ No newline at end of file +count=0 capacity=4 +After shrinkToFit +[] +count=0 capacity=0 \ No newline at end of file From ebf2590654549eafaee93b26d5bbb1ddfbd496e8 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Sun, 12 Sep 2021 14:22:35 -0400 Subject: [PATCH 3/7] Fix strange bug seen with '-O2', '--with-valgrind', and valgrind test I'm guessing this is a bug seen when inlining and one function using assembly and the other not using assembly related to `zend_string_equal_content`? At lower optimization levels it passes. (Environment: gcc 9.3 on Linux) `make test TESTS='ext/spl/tests/Vector/aggregate.phpt -m --show-mem --show-diff` fails while compiling the variable in the foreach. --- Zend/zend_string.c | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Zend/zend_string.c b/Zend/zend_string.c index c24d5dfcdb478..1b5334f3fe77a 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -148,22 +148,7 @@ static zend_always_inline zend_string *zend_interned_string_ht_lookup_ex(zend_ul static zend_always_inline zend_string *zend_interned_string_ht_lookup(zend_string *str, HashTable *interned_strings) { - zend_ulong h = ZSTR_H(str); - uint32_t nIndex; - uint32_t idx; - Bucket *p; - - nIndex = h | interned_strings->nTableMask; - idx = HT_HASH(interned_strings, nIndex); - while (idx != HT_INVALID_IDX) { - p = HT_HASH_TO_BUCKET(interned_strings, idx); - if ((p->h == h) && zend_string_equal_content(p->key, str)) { - return p->key; - } - idx = Z_NEXT(p->val); - } - - return NULL; + return zend_interned_string_ht_lookup_ex(ZSTR_H(str), ZSTR_VAL(str), ZSTR_LEN(str), interned_strings); } /* This function might be not thread safe at least because it would update the From 5431abaf868dc9641602ed05b696a05667ef252a Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Wed, 15 Sep 2021 09:58:59 -0400 Subject: [PATCH 4/7] Add Vector::map/filter implementations --- ext/spl/spl_vector.c | 186 ++++++++++++++++++++++++++++++- ext/spl/spl_vector.stub.php | 13 ++- ext/spl/spl_vector_arginfo.h | 22 +++- ext/spl/tests/Vector/filter.phpt | 58 ++++++++++ ext/spl/tests/Vector/map.phpt | 40 +++++++ 5 files changed, 312 insertions(+), 7 deletions(-) create mode 100644 ext/spl/tests/Vector/filter.phpt create mode 100644 ext/spl/tests/Vector/map.phpt diff --git a/ext/spl/spl_vector.c b/ext/spl/spl_vector.c index 41235f241112e..fcbbe95b40396 100644 --- a/ext/spl/spl_vector.c +++ b/ext/spl/spl_vector.c @@ -983,6 +983,191 @@ PHP_METHOD(Vector, indexOf) RETURN_FALSE; } +PHP_METHOD(Vector, filter) +{ + zend_fcall_info fci = empty_fcall_info; + zend_fcall_info_cache fci_cache = empty_fcall_info_cache; + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_FUNC_OR_NULL(fci, fci_cache) + ZEND_PARSE_PARAMETERS_END(); + + const spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + size_t size = 0; + size_t capacity = 0; + zval *entries = NULL; + zval operand; + if (intern->array.size == 0) { + /* do nothing */ + } else if (ZEND_FCI_INITIALIZED(fci)) { + zval retval; + + fci.params = &operand; + fci.param_count = 1; + fci.retval = &retval; + + for (size_t i = 0; i < intern->array.size; i++) { + ZVAL_COPY(&operand, &intern->array.entries[i]); + const int result = zend_call_function(&fci, &fci_cache); + if (UNEXPECTED(result != SUCCESS || EG(exception))) { + zval_ptr_dtor(&operand); +cleanup: + if (entries) { + for (; size > 0; size--) { + zval_ptr_dtor(&entries[size]); + } + efree(entries); + } + return; + } + const bool is_true = zend_is_true(&retval); + zval_ptr_dtor(&retval); + if (UNEXPECTED(EG(exception))) { + goto cleanup; + } + if (!is_true) { + zval_ptr_dtor(&operand); + if (UNEXPECTED(EG(exception))) { + goto cleanup; + } + continue; + } + + if (size >= capacity) { + if (entries) { + capacity = size + intern->array.size - i; + entries = safe_erealloc(entries, capacity, sizeof(zval), 0); + } else { + ZEND_ASSERT(size == 0); + ZEND_ASSERT(capacity == 0); + capacity = intern->array.size > i ? intern->array.size - i : 1; + entries = safe_emalloc(capacity, sizeof(zval), 0); + } + } + ZEND_ASSERT(size < capacity); + ZVAL_COPY_VALUE(&entries[size], &operand); + size++; + } + } else { + for (size_t i = 0; i < intern->array.size; i++) { + ZVAL_COPY(&operand, &intern->array.entries[i]); + const bool is_true = zend_is_true(&operand); + if (!is_true) { + zval_ptr_dtor(&operand); + if (UNEXPECTED(EG(exception))) { + goto cleanup; + } + continue; + } + + if (size >= capacity) { + if (entries) { + capacity = size + intern->array.size - i; + entries = safe_erealloc(entries, capacity, sizeof(zval), 0); + } else { + ZEND_ASSERT(size == 0); + ZEND_ASSERT(capacity == 0); + capacity = intern->array.size > i ? intern->array.size - i : 1; + entries = safe_emalloc(capacity, sizeof(zval), 0); + } + } + ZEND_ASSERT(size < capacity); + ZVAL_COPY_VALUE(&entries[size], &operand); + size++; + } + } + + zend_object *new_object = spl_vector_new_ex(spl_ce_Vector, NULL, 0); + spl_vector *new_intern = spl_vector_from_object(new_object); + if (size == 0) { + ZEND_ASSERT(!entries); + spl_vector_entries_set_empty_list(&new_intern->array); + RETURN_OBJ(new_object); + } + if (capacity > size) { + /* Shrink allocated value to actual required size */ + entries = erealloc(entries, size * sizeof(zval)); + } + + new_intern->array.entries = entries; + new_intern->array.size = size; + new_intern->array.capacity = size; + RETURN_OBJ(new_object); +} + +PHP_METHOD(Vector, map) +{ + zend_fcall_info fci; + zend_fcall_info_cache fci_cache; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_FUNC(fci, fci_cache) + ZEND_PARSE_PARAMETERS_END(); + + const spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + size_t new_capacity = intern->array.size; + + if (new_capacity == 0) { + zend_object *new_object = spl_vector_new_ex(spl_ce_Vector, NULL, 0); + spl_vector *new_intern = spl_vector_from_object(new_object); + spl_vector_entries_set_empty_list(&new_intern->array); + RETURN_OBJ(new_object); + } + + zval *entries = emalloc(new_capacity * sizeof(zval)); + size_t size = 0; + + zval operand; + fci.params = &operand; + fci.param_count = 1; + + do { + if (UNEXPECTED(size >= new_capacity)) { + if (entries) { + new_capacity = size + 1; + entries = safe_erealloc(entries, new_capacity, sizeof(zval), 0); + } else { + ZEND_ASSERT(size == 0); + ZEND_ASSERT(new_capacity == 0); + new_capacity = intern->array.size; + entries = safe_emalloc(new_capacity, sizeof(zval), 0); + } + } + fci.retval = &entries[size]; + ZVAL_COPY(&operand, &intern->array.entries[size]); + const int result = zend_call_function(&fci, &fci_cache); + fci.retval = &entries[size]; + zval_ptr_dtor(&operand); + if (UNEXPECTED(result != SUCCESS || EG(exception))) { +cleanup: + if (entries) { + for (; size > 0; size--) { + zval_ptr_dtor(&entries[size]); + } + efree(entries); + } + return; + } + size++; + } while (size < intern->array.size); + + zend_object *new_object = spl_vector_new_ex(spl_ce_Vector, NULL, 0); + spl_vector *new_intern = spl_vector_from_object(new_object); + if (size == 0) { + ZEND_ASSERT(!entries); + spl_vector_entries_set_empty_list(&new_intern->array); + RETURN_OBJ(new_object); + } + if (UNEXPECTED(new_capacity > size)) { + /* Shrink allocated value to actual required size */ + entries = erealloc(entries, size * sizeof(zval)); + } + + new_intern->array.entries = entries; + new_intern->array.size = size; + new_intern->array.capacity = size; + RETURN_OBJ(new_object); +} + PHP_METHOD(Vector, contains) { zval *value; @@ -1213,7 +1398,6 @@ PHP_MINIT_FUNCTION(spl_vector) spl_handler_Vector.count_elements = spl_vector_count_elements; spl_handler_Vector.get_properties = spl_vector_get_properties; spl_handler_Vector.get_gc = spl_vector_get_gc; - spl_handler_Vector.dtor_obj = zend_objects_destroy_object; spl_handler_Vector.free_obj = spl_vector_free_storage; spl_handler_Vector.read_dimension = spl_vector_read_dimension; diff --git a/ext/spl/spl_vector.stub.php b/ext/spl/spl_vector.stub.php index 7067a594b2287..ce1afe590356c 100644 --- a/ext/spl/spl_vector.stub.php +++ b/ext/spl/spl_vector.stub.php @@ -15,6 +15,7 @@ public function __construct(iterable $iterator = [], bool $preserveKeys = true) public function getIterator(): InternalIterator {} public function count(): int {} public function capacity(): int {} + public function shrinkToFit(): void {} public function clear(): void {} public function setSize(int $size): void {} @@ -39,7 +40,17 @@ public function offsetUnset(mixed $offset): void {} public function indexOf(mixed $value): int|false {} public function contains(mixed $value): bool {} - public function shrinkToFit(): void {} + public function map(callable $callback): Vector {} + /** + * Returns the subset of elements of the Vector satisfying the predicate. + * + * If the value returned by the callback is truthy + * (e.g. true, non-zero number, non-empty array, truthy object, etc.), + * this is treated as satisfying the predicate. + * + * (at)param null|callable(mixed):mixed $callback + */ + public function filter(?callable $callback = null): Vector {} public function jsonSerialize(): array {} } diff --git a/ext/spl/spl_vector_arginfo.h b/ext/spl/spl_vector_arginfo.h index 9dda90f830daa..619614235f560 100644 --- a/ext/spl/spl_vector_arginfo.h +++ b/ext/spl/spl_vector_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: b283cbbdbd13b6231744dff4b39dd648a889082e */ + * Stub hash: 67a8a475004563eb10b9c117d32b864c94c13c38 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Vector___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, iterator, IS_ITERABLE, 0, "[]") @@ -14,9 +14,11 @@ ZEND_END_ARG_INFO() #define arginfo_class_Vector_capacity arginfo_class_Vector_count -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_clear, 0, 0, IS_VOID, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_shrinkToFit, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() +#define arginfo_class_Vector_clear arginfo_class_Vector_shrinkToFit + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_setSize, 0, 1, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, size, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -75,7 +77,13 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_contains, 0, 1, _IS ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) ZEND_END_ARG_INFO() -#define arginfo_class_Vector_shrinkToFit arginfo_class_Vector_clear +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Vector_map, 0, 1, Vector, 0) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Vector_filter, 0, 0, Vector, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, callback, IS_CALLABLE, 1, "null") +ZEND_END_ARG_INFO() #define arginfo_class_Vector_jsonSerialize arginfo_class_Vector___serialize @@ -84,6 +92,7 @@ ZEND_METHOD(Vector, __construct); ZEND_METHOD(Vector, getIterator); ZEND_METHOD(Vector, count); ZEND_METHOD(Vector, capacity); +ZEND_METHOD(Vector, shrinkToFit); ZEND_METHOD(Vector, clear); ZEND_METHOD(Vector, setSize); ZEND_METHOD(Vector, __serialize); @@ -100,7 +109,8 @@ ZEND_METHOD(Vector, offsetSet); ZEND_METHOD(Vector, offsetUnset); ZEND_METHOD(Vector, indexOf); ZEND_METHOD(Vector, contains); -ZEND_METHOD(Vector, shrinkToFit); +ZEND_METHOD(Vector, map); +ZEND_METHOD(Vector, filter); ZEND_METHOD(Vector, jsonSerialize); @@ -109,6 +119,7 @@ static const zend_function_entry class_Vector_methods[] = { ZEND_ME(Vector, getIterator, arginfo_class_Vector_getIterator, ZEND_ACC_PUBLIC) ZEND_ME(Vector, count, arginfo_class_Vector_count, ZEND_ACC_PUBLIC) ZEND_ME(Vector, capacity, arginfo_class_Vector_capacity, ZEND_ACC_PUBLIC) + ZEND_ME(Vector, shrinkToFit, arginfo_class_Vector_shrinkToFit, ZEND_ACC_PUBLIC) ZEND_ME(Vector, clear, arginfo_class_Vector_clear, ZEND_ACC_PUBLIC) ZEND_ME(Vector, setSize, arginfo_class_Vector_setSize, ZEND_ACC_PUBLIC) ZEND_ME(Vector, __serialize, arginfo_class_Vector___serialize, ZEND_ACC_PUBLIC) @@ -125,7 +136,8 @@ static const zend_function_entry class_Vector_methods[] = { ZEND_ME(Vector, offsetUnset, arginfo_class_Vector_offsetUnset, ZEND_ACC_PUBLIC) ZEND_ME(Vector, indexOf, arginfo_class_Vector_indexOf, ZEND_ACC_PUBLIC) ZEND_ME(Vector, contains, arginfo_class_Vector_contains, ZEND_ACC_PUBLIC) - ZEND_ME(Vector, shrinkToFit, arginfo_class_Vector_shrinkToFit, ZEND_ACC_PUBLIC) + ZEND_ME(Vector, map, arginfo_class_Vector_map, ZEND_ACC_PUBLIC) + ZEND_ME(Vector, filter, arginfo_class_Vector_filter, ZEND_ACC_PUBLIC) ZEND_ME(Vector, jsonSerialize, arginfo_class_Vector_jsonSerialize, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/ext/spl/tests/Vector/filter.phpt b/ext/spl/tests/Vector/filter.phpt new file mode 100644 index 0000000000000..379293b47158e --- /dev/null +++ b/ext/spl/tests/Vector/filter.phpt @@ -0,0 +1,58 @@ +--TEST-- +Vector filter() +--FILE-- +count(), $v->capacity()); + foreach ($v as $i => $value) { + printf("Value #%d: %s\n", $i, var_export($value, true)); + } +} +dump_vector((new Vector([false]))->filter()); +dump_vector((new Vector([true]))->filter()); +dump_vector((new Vector([strtoupper('test'), true, false, new stdClass()]))->filter()); +echo "Test functions\n"; +$negate = fn($x) => !$x; +dump_vector((new Vector([]))->filter($negate)); +dump_vector((new Vector([true]))->filter($negate)); +dump_vector((new Vector([strtoupper('test'), true, false, new stdClass(), []]))->filter($negate)); +$identity = fn($x) => $x; +dump_vector((new Vector([]))->filter($identity)); +dump_vector((new Vector([true]))->filter($identity)); +dump_vector((new Vector([strtoupper('test'), true, false, new stdClass(), []]))->filter($identity)); +try { + (new Vector([strtoupper('first'), 'not reached']))->filter(function ($parameter) { + var_dump($parameter); + throw new RuntimeException("test"); + }); + echo "Should throw\n"; +} catch (Exception $e) { + printf("Caught %s: %s\n", get_class($e), $e->getMessage()); +} +?> +--EXPECT-- +count=0 capacity=0 +count=1 capacity=1 +Value #0: true +count=3 capacity=3 +Value #0: 'TEST' +Value #1: true +Value #2: (object) array( +) +Test functions +count=0 capacity=0 +count=0 capacity=0 +count=2 capacity=2 +Value #0: false +Value #1: array ( +) +count=0 capacity=0 +count=1 capacity=1 +Value #0: true +count=3 capacity=3 +Value #0: 'TEST' +Value #1: true +Value #2: (object) array( +) +string(5) "FIRST" +Caught RuntimeException: test diff --git a/ext/spl/tests/Vector/map.phpt b/ext/spl/tests/Vector/map.phpt new file mode 100644 index 0000000000000..486dfca80feae --- /dev/null +++ b/ext/spl/tests/Vector/map.phpt @@ -0,0 +1,40 @@ +--TEST-- +Vector map() +--FILE-- +count(), $v->capacity()); + foreach ($v as $i => $value) { + printf("Value #%d: %s\n", $i, json_encode($value)); + } +} +$create_array = fn($x) => [$x]; +dump_vector((new Vector())->map($create_array)); +dump_vector((new Vector([strtoupper('test'), true, false, new stdClass(), []]))->map($create_array)); +$identity = fn($x) => $x; +dump_vector((new Vector())->map($identity)); +dump_vector((new Vector([strtoupper('test'), true, false, new stdClass(), []]))->map($identity)); +$false = fn() => false; +dump_vector((new Vector([strtoupper('test'), true, false, new stdClass(), []]))->map($false)); +?> +--EXPECT-- +count=0 capacity=0 +count=5 capacity=5 +Value #0: ["TEST"] +Value #1: [true] +Value #2: [false] +Value #3: [{}] +Value #4: [[]] +count=0 capacity=0 +count=5 capacity=5 +Value #0: "TEST" +Value #1: true +Value #2: false +Value #3: {} +Value #4: [] +count=5 capacity=5 +Value #0: false +Value #1: false +Value #2: false +Value #3: false +Value #4: false \ No newline at end of file From b67095c02497da188472fffe55bfb6273ff60b14 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Wed, 22 Sep 2021 20:38:37 -0400 Subject: [PATCH 5/7] WIP reduce public api --- ext/spl/spl_vector.c | 230 ++++++++++++++++-------------------- ext/spl/spl_vector.stub.php | 70 +++++++++-- 2 files changed, 166 insertions(+), 134 deletions(-) diff --git a/ext/spl/spl_vector.c b/ext/spl/spl_vector.c index fcbbe95b40396..ce529f75d868c 100644 --- a/ext/spl/spl_vector.c +++ b/ext/spl/spl_vector.c @@ -118,7 +118,7 @@ typedef struct _spl_vector_it { zend_long current; } spl_vector_it; -static void spl_vector_raise_capacity(spl_vector *intern, const zend_long new_capacity); +static void spl_vector_raise_capacity(spl_vector *intern, const size_t new_capacity); static spl_vector *spl_vector_from_object(zend_object *obj) { @@ -164,8 +164,9 @@ static bool spl_vector_entries_uninitialized(const spl_vector_entries *array) return false; } -static void spl_vector_raise_capacity(spl_vector *intern, const zend_long new_capacity) { +static void spl_vector_raise_capacity(spl_vector *intern, const size_t new_capacity) { ZEND_ASSERT(new_capacity > intern->array.capacity); + ZEND_ASSERT(new_capacity <= MAX_VALID_OFFSET + 1); if (intern->array.capacity == 0) { intern->array.entries = safe_emalloc(new_capacity, sizeof(zval), 0); } else { @@ -212,7 +213,7 @@ static inline void spl_vector_entries_set_empty_list(spl_vector_entries *array) static void spl_vector_entries_init_from_array(spl_vector_entries *array, zend_array *values, bool preserve_keys) { - const zend_long num_elements = zend_hash_num_elements(values); + const uint32_t num_elements = zend_hash_num_elements(values); if (num_elements <= 0) { spl_vector_entries_set_empty_list(array); return; @@ -227,7 +228,7 @@ static void spl_vector_entries_init_from_array(spl_vector_entries *array, zend_a zend_ulong num_index, max_index = 0; ZEND_HASH_FOREACH_KEY(values, num_index, str_index) { - if (str_index != NULL || (zend_long)num_index < 0) { + if (UNEXPECTED(str_index != NULL || (zend_long)num_index < 0)) { zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "array must contain only positive integer keys"); return; } @@ -237,18 +238,19 @@ static void spl_vector_entries_init_from_array(spl_vector_entries *array, zend_a } } ZEND_HASH_FOREACH_END(); - const zend_long size = max_index + 1; - if (size <= 0) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "integer overflow detected"); + if (UNEXPECTED(max_index > MAX_VALID_OFFSET)) { + zend_error_noreturn("exceeded max valid offset of Vector"); return; } + const zend_ulong size = max_index + 1; + ZEND_ASSERT(size > 0); array->entries = entries = safe_emalloc(size, sizeof(zval), 0); array->size = size; array->capacity = size; /* Optimization: Don't need to null remaining elements if all elements from 0..num_elements-1 are set. */ ZEND_ASSERT(size >= num_elements); if (size > num_elements) { - for (zend_long i = 0; i < size; i++) { + for (uint32_t i = 0; i < size; i++) { ZVAL_NULL(&entries[i]); } } @@ -256,12 +258,14 @@ static void spl_vector_entries_init_from_array(spl_vector_entries *array, zend_a ZEND_HASH_FOREACH_KEY_VAL(values, num_index, str_index, val) { ZEND_ASSERT(num_index < size); ZEND_ASSERT(!str_index); + /* should happen for non-corrupt array inputs */ + ZEND_ASSERT(size == num_elements || Z_TYPE(entries[num_index]) == IS_NULL); ZVAL_COPY_DEREF(&entries[num_index], val); } ZEND_HASH_FOREACH_END(); return; } - int i = 0; + size_t i = 0; array->entries = entries = safe_emalloc(num_elements, sizeof(zval), 0); array->size = num_elements; array->capacity = num_elements; @@ -272,7 +276,7 @@ static void spl_vector_entries_init_from_array(spl_vector_entries *array, zend_a } ZEND_HASH_FOREACH_END(); } -static void spl_vector_entries_init_from_traversable(spl_vector_entries *array, zend_object *obj, bool preserve_keys) +static void spl_vector_entries_init_from_traversable(spl_vector_entries *array, zend_object *obj) { zend_class_entry *ce = obj->ce; zend_object_iterator *iter; @@ -297,95 +301,36 @@ static void spl_vector_entries_init_from_traversable(spl_vector_entries *array, } } - if (preserve_keys) { - if (!funcs->get_current_key) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Saw traversable without keys"); - return; + /* Reindex keys from 0. */ + while (funcs->valid(iter) == SUCCESS) { + if (EG(exception)) { + break; } - - /* size is 0 or max_index + 1 */ - while (funcs->valid(iter) == SUCCESS) { - if (UNEXPECTED(EG(exception))) { - break; - } - - zval key; - funcs->get_current_key(iter, &key); - if (UNEXPECTED(EG(exception))) { - break; - } - if (Z_TYPE(key) != IS_LONG || Z_LVAL(key) < 0) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "array must contain only positive integer keys"); - break; - } - const zend_long num_index = Z_LVAL(key); - if (num_index >= capacity) { - if (UNEXPECTED((zend_ulong) num_index > ZEND_LONG_MAX / 2 - 1)) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "integer overflow detected"); - break; - } - const zend_long new_capacity = (num_index + 1) * 2; - entries = safe_erealloc(entries, new_capacity, sizeof(zval), 0); - for ( ; capacity < new_capacity; capacity++) { - ZVAL_NULL(&entries[capacity]); - } - } - - zval *value = funcs->get_current_data(iter); - if (UNEXPECTED(EG(exception))) { - break; - } - - if (num_index >= size) { - size = num_index + 1; - ZEND_ASSERT(size <= capacity); - } - - /* Allow Traversables such as Generators to repeat keys. Silently overwrite the old key. */ - zval_ptr_dtor(&entries[num_index]); - if (UNEXPECTED(EG(exception))) { - break; - } - ZVAL_COPY_DEREF(&entries[num_index], value); - - iter->index++; - funcs->move_forward(iter); - if (UNEXPECTED(EG(exception))) { - break; - } + zval *value = funcs->get_current_data(iter); + if (UNEXPECTED(EG(exception))) { + break; + } + if (UNEXPECTED(EG(exception))) { + break; } - } else { - /* Reindex keys from 0. */ - while (funcs->valid(iter) == SUCCESS) { - if (EG(exception)) { - break; - } - zval *value = funcs->get_current_data(iter); - if (UNEXPECTED(EG(exception))) { - break; - } - 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 */ - if (entries) { - capacity *= 2; - entries = safe_erealloc(entries, capacity, sizeof(zval), 0); - } else { - capacity = 4; - entries = safe_emalloc(capacity, sizeof(zval), 0); - } + if (size >= capacity) { + /* Not using Countable::count(), that would potentially have side effects or throw UnsupportedOperationException or be slow to compute */ + if (entries) { + capacity *= 2; + entries = safe_erealloc(entries, capacity, sizeof(zval), 0); + } else { + capacity = 4; + entries = safe_emalloc(capacity, sizeof(zval), 0); } - ZVAL_COPY_DEREF(&entries[size], value); - size++; + } + ZVAL_COPY_DEREF(&entries[size], value); + size++; - iter->index++; - funcs->move_forward(iter); - if (UNEXPECTED(EG(exception))) { - break; - } + iter->index++; + funcs->move_forward(iter); + if (UNEXPECTED(EG(exception))) { + break; } } if (capacity > size) { @@ -406,7 +351,9 @@ static void spl_vector_entries_init_from_traversable(spl_vector_entries *array, */ static void spl_vector_copy_range(spl_vector_entries *array, size_t offset, zval *begin, zval *end) { - ZEND_ASSERT(array->size - offset >= end - begin); + ZEND_ASSERT(offset <= array->size); + ZEND_ASSERT(begin <= end); + ZEND_ASSERT(array->size - offset >= (size_t)(end - begin)); zval *to = &array->entries[offset]; while (begin != end) { @@ -555,17 +502,6 @@ PHP_METHOD(Vector, count) RETURN_LONG(intern->array.size); } -/* Get capacity of this vector */ -PHP_METHOD(Vector, capacity) -{ - zval *object = ZEND_THIS; - - ZEND_PARSE_PARAMETERS_NONE(); - - const spl_vector *intern = Z_VECTOR_P(object); - RETURN_LONG(intern->array.capacity); -} - /* Free elements and backing storage of this vector */ PHP_METHOD(Vector, clear) { @@ -588,26 +524,39 @@ PHP_METHOD(Vector, clear) PHP_METHOD(Vector, setSize) { zend_long size; - ZEND_PARSE_PARAMETERS_START(1, 1) + zval *default_zval = NULL; + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_LONG(size) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_zval) ZEND_PARSE_PARAMETERS_END(); - if (size < 0) { - zend_argument_value_error(1, "must be greater than or equal to 0"); + if (UNEXPECTED((zend_ulong)size > MAX_VALID_OFFSET + 1)) { + if (size < 0) { + zend_argument_value_error(1, "must be greater than or equal to 0"); + } else { + zend_error_noreturn("exceeded max valid Teds\\Vector offset"); + } RETURN_THROWS(); } spl_vector *intern = Z_VECTOR_P(ZEND_THIS); const size_t old_size = intern->array.size; - if (size > old_size) { + if ((zend_ulong) size > old_size) { /* Raise the capacity as needed and fill the space with nulls */ - if (size > intern->array.capacity) { + if ((zend_ulong) size > intern->array.capacity) { spl_vector_raise_capacity(intern, size); } intern->array.size = size; zval * const entries = intern->array.entries; - for (size_t i = old_size; i < size; i++) { - ZVAL_NULL(&entries[i]); + if (default_zval == NULL || Z_ISNULL_P(default_zval)) { + for (zend_long i = old_size; i < size; i++) { + ZVAL_NULL(&entries[i]); + } + } else { + for (zend_long i = old_size; i < size; i++) { + ZVAL_COPY(&entries[i], default_zval); + } } return; } @@ -625,7 +574,8 @@ PHP_METHOD(Vector, setSize) old_copy = spl_zval_copy_range(&old_entries[size], entries_to_remove); intern->array.size = size; - if (size * 4 < intern->array.capacity) { + /* If only a quarter of the reserved space is used, then shrink the capacity but leave some room to grow. */ + if ((zend_ulong) size < (intern->array.capacity >> 2)) { const size_t size = old_size - 1; const size_t capacity = size > 2 ? size * 2 : 4; if (capacity < intern->array.capacity) { @@ -696,7 +646,7 @@ static int spl_vector_it_valid(zend_object_iterator *iter) spl_vector_it *iterator = (spl_vector_it*)iter; spl_vector *object = Z_VECTOR_P(&iter->data); - if (iterator->current >= 0 && iterator->current < object->array.size) { + if (iterator->current >= 0 && ((zend_ulong) iterator->current) < object->array.size) { return SUCCESS; } @@ -927,7 +877,7 @@ static zend_always_inline void spl_vector_get_value_at_offset(zval *return_value RETURN_COPY(&intern->array.entries[offset]); } -PHP_METHOD(Vector, valueAt) +PHP_METHOD(Vector, get) { zend_long offset; ZEND_PARSE_PARAMETERS_START(1, 1) @@ -980,7 +930,7 @@ PHP_METHOD(Vector, indexOf) RETURN_LONG(i); } } - RETURN_FALSE; + RETURN_NULL(); } PHP_METHOD(Vector, filter) @@ -1138,7 +1088,6 @@ PHP_METHOD(Vector, map) fci.retval = &entries[size]; zval_ptr_dtor(&operand); if (UNEXPECTED(result != SUCCESS || EG(exception))) { -cleanup: if (entries) { for (; size > 0; size--) { zval_ptr_dtor(&entries[size]); @@ -1201,7 +1150,7 @@ static zend_always_inline void spl_vector_set_value_at_offset(zend_object *objec zval_ptr_dtor(&tmp); } -PHP_METHOD(Vector, setValueAt) +PHP_METHOD(Vector, set) { zend_long offset; zval *value; @@ -1241,15 +1190,44 @@ static zend_always_inline void spl_vector_push(spl_vector *intern, zval *value) intern->array.size++; } +/* Based on array_push */ PHP_METHOD(Vector, push) { - zval *value; + const zval *args; + uint32_t argc; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('+', args, argc) ZEND_PARSE_PARAMETERS_END(); - spl_vector_push(Z_VECTOR_P(ZEND_THIS), value); + if (argc == 0) { + return; + } + spl_vector *intern = Z_VECTOR_P(ZEND_THIS); + const size_t old_size = intern->array.size; + const size_t new_size = old_size + argc; + /* The compiler will type check but eliminate dead code on platforms where size_t is 32 bits (4 bytes) */ + if (SIZEOF_SIZE_T < 8 && UNEXPECTED(new_size > MAX_VALID_OFFSET + 1 || new_size < old_size)) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "exceeded max valid offset"); + RETURN_THROWS(); + } + const size_t old_capacity = intern->array.capacity; + if (new_size > old_capacity) { + size_t new_capacity = old_size ? old_size * 2 : 4; + if (UNEXPECTED(new_size > new_capacity)) { + new_capacity = new_size + (new_size >> 1); + } + if (SIZEOF_SIZE_T < 8 && UNEXPECTED(new_capacity > MAX_VALID_OFFSET + 1)) { + new_capacity = MAX_VALID_OFFSET + 1; + } + spl_vector_raise_capacity(intern, new_capacity); + } + zval *entries = intern->array.entries; + + for (uint32_t i = 0; i < argc; i++) { + ZVAL_COPY(&entries[old_size + i], &args[i]); + } + intern->array.size = new_size; } PHP_METHOD(Vector, pop) @@ -1332,7 +1310,7 @@ static void spl_vector_write_dimension(zend_object *object, zval *offset_zv, zva zend_long offset; CONVERT_OFFSET_TO_LONG_OR_THROW(offset, offset_zv); - if (offset < 0 || offset >= intern->array.size) { + if (offset < 0 || (zend_ulong) offset >= intern->array.size) { zend_throw_exception(spl_ce_OutOfBoundsException, "Index invalid or out of range", 0); return; } @@ -1351,7 +1329,7 @@ static zval *spl_vector_read_dimension(zend_object *object, zval *offset_zv, int const spl_vector *intern = spl_vector_from_object(object); - if (UNEXPECTED(offset < 0 || offset >= intern->array.size)) { + if (UNEXPECTED(offset < 0 || (zend_ulong) offset >= intern->array.size)) { if (type != BP_VAR_IS) { zend_throw_exception(spl_ce_OutOfBoundsException, "Index out of range", 0); } @@ -1375,7 +1353,7 @@ static int spl_vector_has_dimension(zend_object *object, zval *offset_zv, int ch const spl_vector *intern = spl_vector_from_object(object); - if (UNEXPECTED(offset < 0 || offset >= intern->array.size)) { + if (UNEXPECTED(((zend_ulong) offset) >= intern->array.size || offset < 0)) { return 0; } diff --git a/ext/spl/spl_vector.stub.php b/ext/spl/spl_vector.stub.php index ce1afe590356c..c8994038d6cee 100644 --- a/ext/spl/spl_vector.stub.php +++ b/ext/spl/spl_vector.stub.php @@ -2,6 +2,16 @@ /** @generate-class-entries */ +/** + * A Vector is a container of a sequence of values (with keys 0, 1, ...count($vector) - 1) + * that can change in size. + * + * This is backed by a memory-efficient representation + * (raw array of values) and provides fast access (compared to other objects in the Spl) + * and constant amortized-time push/pop operations. + * + * Attempting to read or write values outside of the range of values with `*get`/`*set` methods will throw at runtime. + */ final class Vector implements IteratorAggregate, Countable, JsonSerializable, ArrayAccess { /** @@ -12,34 +22,78 @@ final class Vector implements IteratorAggregate, Countable, JsonSerializable, Ar * and negative indices or non-integer indices will be rejected and cause an Exception. */ public function __construct(iterable $iterator = [], bool $preserveKeys = true) {} + /** + * Returns an iterator that will return the indexes and values of iterable until index >= count() + */ public function getIterator(): InternalIterator {} + /** + * Returns the number of values in this Vector + */ public function count(): int {} - public function capacity(): int {} + /** + * Reduces the Vector's capacity to its size, freeing any extra unused memory. + */ public function shrinkToFit(): void {} + /** + * Remove all elements from the array and free all reserved capacity. + */ public function clear(): void {} - public function setSize(int $size): void {} + + /** + * If $size is greater than the current size, raise the size and fill the free space with $value + * If $size is less than the current size, reduce the size and discard elements. + */ + public function setSize(int $size, mixed $value = null): void {} public function __serialize(): array {} public function __unserialize(array $data): void {} public static function __set_state(array $array): Vector {} - public function push(mixed $value): void {} + public function push(mixed ...$values): void {} public function pop(): mixed {} public function toArray(): array {} // Strictly typed, unlike offsetGet/offsetSet - public function valueAt(int $offset): mixed {} - public function setValueAt(int $offset, mixed $value): void {} + public function get(int $offset): mixed {} + public function set(int $offset, mixed $value): void {} + /** + * Returns the value at (int)$offset. + * @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 (int)$offset 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 {} - // Throws because unset and null are different things, unlike SplFixedArray + + /** + * @throws \RuntimeException unconditionally because unset and null are different things, unlike SplFixedArray + */ public function offsetUnset(mixed $offset): void {} - public function indexOf(mixed $value): int|false {} + /** + * Returns the offset of a value that is === $value, or returns null. + */ + public function indexOf(mixed $value): ?int {} + /** + * @return bool true if there exists a value === $value in this vector. + */ public function contains(mixed $value): bool {} + /** + * Returns a new Vector instance created from the return values of $callable($element) + * being applied to each element of this vector. + * + * (at)param null|callable(mixed):mixed $callback + */ public function map(callable $callback): Vector {} /** * Returns the subset of elements of the Vector satisfying the predicate. @@ -48,7 +102,7 @@ public function map(callable $callback): Vector {} * (e.g. true, non-zero number, non-empty array, truthy object, etc.), * this is treated as satisfying the predicate. * - * (at)param null|callable(mixed):mixed $callback + * (at)param null|callable(mixed):bool $callback */ public function filter(?callable $callback = null): Vector {} From f3cea215637c293ec98be8604aa3a513e2989d84 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Wed, 22 Sep 2021 22:19:02 -0400 Subject: [PATCH 6/7] wip --- ext/spl/spl_vector.stub.php | 6 ++---- ext/spl/spl_vector_arginfo.h | 26 +++++++++++--------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/ext/spl/spl_vector.stub.php b/ext/spl/spl_vector.stub.php index c8994038d6cee..3f3cbc4fd4d39 100644 --- a/ext/spl/spl_vector.stub.php +++ b/ext/spl/spl_vector.stub.php @@ -17,11 +17,9 @@ final class Vector implements IteratorAggregate, Countable, JsonSerializable, Ar /** * Construct a Vector from an iterable. * - * When $preserveKeys is false, the values will be reindexed without gaps starting from 0 - * When $preserveKeys is true, any gaps in the keys of the iterable will be filled in with null, - * and negative indices or non-integer indices will be rejected and cause an Exception. + * The keys will be ignored, and values will be reindexed without gaps starting from 0 */ - public function __construct(iterable $iterator = [], bool $preserveKeys = true) {} + public function __construct(iterable $iterator = []) {} /** * Returns an iterator that will return the indexes and values of iterable until index >= count() */ diff --git a/ext/spl/spl_vector_arginfo.h b/ext/spl/spl_vector_arginfo.h index 619614235f560..1a5eb739c2a51 100644 --- a/ext/spl/spl_vector_arginfo.h +++ b/ext/spl/spl_vector_arginfo.h @@ -1,9 +1,8 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 67a8a475004563eb10b9c117d32b864c94c13c38 */ + * Stub hash: 92769a3b5fa4af0222ab47445b174406288c303d */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Vector___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, iterator, IS_ITERABLE, 0, "[]") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, preserveKeys, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Vector_getIterator, 0, 0, InternalIterator, 0) @@ -12,8 +11,6 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_count, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() -#define arginfo_class_Vector_capacity arginfo_class_Vector_count - ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_shrinkToFit, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() @@ -21,6 +18,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_setSize, 0, 1, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, size, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, value, IS_MIXED, 0, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector___serialize, 0, 0, IS_ARRAY, 0) @@ -34,8 +32,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Vector___set_state, 0, 1, V ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_push, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_push, 0, 0, IS_VOID, 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_Vector_pop, 0, 0, IS_MIXED, 0) @@ -43,11 +41,11 @@ ZEND_END_ARG_INFO() #define arginfo_class_Vector_toArray arginfo_class_Vector___serialize -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_valueAt, 0, 1, IS_MIXED, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_get, 0, 1, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_setValueAt, 0, 2, IS_VOID, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_set, 0, 2, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) ZEND_END_ARG_INFO() @@ -69,7 +67,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_offsetUnset, 0, 1, ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_Vector_indexOf, 0, 1, MAY_BE_LONG|MAY_BE_FALSE) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Vector_indexOf, 0, 1, IS_LONG, 1) ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) ZEND_END_ARG_INFO() @@ -91,7 +89,6 @@ ZEND_END_ARG_INFO() ZEND_METHOD(Vector, __construct); ZEND_METHOD(Vector, getIterator); ZEND_METHOD(Vector, count); -ZEND_METHOD(Vector, capacity); ZEND_METHOD(Vector, shrinkToFit); ZEND_METHOD(Vector, clear); ZEND_METHOD(Vector, setSize); @@ -101,8 +98,8 @@ ZEND_METHOD(Vector, __set_state); ZEND_METHOD(Vector, push); ZEND_METHOD(Vector, pop); ZEND_METHOD(Vector, toArray); -ZEND_METHOD(Vector, valueAt); -ZEND_METHOD(Vector, setValueAt); +ZEND_METHOD(Vector, get); +ZEND_METHOD(Vector, set); ZEND_METHOD(Vector, offsetGet); ZEND_METHOD(Vector, offsetExists); ZEND_METHOD(Vector, offsetSet); @@ -118,7 +115,6 @@ static const zend_function_entry class_Vector_methods[] = { ZEND_ME(Vector, __construct, arginfo_class_Vector___construct, ZEND_ACC_PUBLIC) ZEND_ME(Vector, getIterator, arginfo_class_Vector_getIterator, ZEND_ACC_PUBLIC) ZEND_ME(Vector, count, arginfo_class_Vector_count, ZEND_ACC_PUBLIC) - ZEND_ME(Vector, capacity, arginfo_class_Vector_capacity, ZEND_ACC_PUBLIC) ZEND_ME(Vector, shrinkToFit, arginfo_class_Vector_shrinkToFit, ZEND_ACC_PUBLIC) ZEND_ME(Vector, clear, arginfo_class_Vector_clear, ZEND_ACC_PUBLIC) ZEND_ME(Vector, setSize, arginfo_class_Vector_setSize, ZEND_ACC_PUBLIC) @@ -128,8 +124,8 @@ static const zend_function_entry class_Vector_methods[] = { ZEND_ME(Vector, push, arginfo_class_Vector_push, ZEND_ACC_PUBLIC) ZEND_ME(Vector, pop, arginfo_class_Vector_pop, ZEND_ACC_PUBLIC) ZEND_ME(Vector, toArray, arginfo_class_Vector_toArray, ZEND_ACC_PUBLIC) - ZEND_ME(Vector, valueAt, arginfo_class_Vector_valueAt, ZEND_ACC_PUBLIC) - ZEND_ME(Vector, setValueAt, arginfo_class_Vector_setValueAt, ZEND_ACC_PUBLIC) + ZEND_ME(Vector, get, arginfo_class_Vector_get, ZEND_ACC_PUBLIC) + ZEND_ME(Vector, set, arginfo_class_Vector_set, ZEND_ACC_PUBLIC) ZEND_ME(Vector, offsetGet, arginfo_class_Vector_offsetGet, ZEND_ACC_PUBLIC) ZEND_ME(Vector, offsetExists, arginfo_class_Vector_offsetExists, ZEND_ACC_PUBLIC) ZEND_ME(Vector, offsetSet, arginfo_class_Vector_offsetSet, ZEND_ACC_PUBLIC) From 1ac4c83b8082ea66d76537ca9ec65647b23ae7db Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Wed, 22 Sep 2021 22:22:03 -0400 Subject: [PATCH 7/7] Update API after RFC discussion --- ext/spl/spl_vector.c | 138 +++++++----------- ext/spl/tests/Vector/Vector.phpt | 49 +++---- ext/spl/tests/Vector/aggregate.phpt | 16 +- ext/spl/tests/Vector/arrayCast.phpt | 14 +- ext/spl/tests/Vector/clear.phpt | 18 +-- ext/spl/tests/Vector/clone.phpt | 6 +- ext/spl/tests/Vector/contains.phpt | 10 +- ext/spl/tests/Vector/exceptionhandler.phpt | 2 +- ext/spl/tests/Vector/filter.phpt | 20 +-- ext/spl/tests/Vector/isset.phpt | 21 +++ ext/spl/tests/Vector/map.phpt | 12 +- ext/spl/tests/Vector/offsetGet.phpt | 60 ++++---- ext/spl/tests/Vector/push_pop.phpt | 68 +++++---- ext/spl/tests/Vector/reinit_forbidden.phpt | 10 +- ext/spl/tests/Vector/serialization.phpt | 12 +- ext/spl/tests/Vector/setSize.phpt | 26 ++-- ext/spl/tests/Vector/setSize_default.phpt | 24 +++ ext/spl/tests/Vector/setValueAt.phpt | 22 +-- ext/spl/tests/Vector/set_state.phpt | 10 +- ext/spl/tests/Vector/shrink_capacity.phpt | 48 +++--- ext/spl/tests/Vector/toArray.phpt | 36 ++++- ext/spl/tests/Vector/traversable.phpt | 31 ++-- .../Vector/traversable_preserve_keys.phpt | 48 ------ 23 files changed, 340 insertions(+), 361 deletions(-) create mode 100644 ext/spl/tests/Vector/isset.phpt create mode 100644 ext/spl/tests/Vector/setSize_default.phpt delete mode 100644 ext/spl/tests/Vector/traversable_preserve_keys.phpt diff --git a/ext/spl/spl_vector.c b/ext/spl/spl_vector.c index ce529f75d868c..f349a6fea85b8 100644 --- a/ext/spl/spl_vector.c +++ b/ext/spl/spl_vector.c @@ -35,6 +35,10 @@ #include +/* Though rare, it is possible to have 64-bit zend_longs and a 32-bit size_t. */ +#define MAX_ZVAL_COUNT ((SIZE_MAX / sizeof(zval)) - 1) +#define MAX_VALID_OFFSET ((size_t)(MAX_ZVAL_COUNT > ZEND_LONG_MAX ? ZEND_LONG_MAX : MAX_ZVAL_COUNT)) + /* Miscellaneous utility functions */ static inline zval *spl_zval_copy_range(const zval *original, size_t n) { const size_t bytes = n * sizeof(zval); @@ -120,6 +124,15 @@ typedef struct _spl_vector_it { static void spl_vector_raise_capacity(spl_vector *intern, const size_t new_capacity); +/* + * If a size this large is encountered, assume the allocation will likely fail or + * future changes to the capacity will overflow. + */ +static ZEND_COLD void spl_error_noreturn_max_vector_capacity() +{ + zend_error_noreturn(E_ERROR, "exceeded max valid Vector capacity"); +} + static spl_vector *spl_vector_from_object(zend_object *obj) { return (spl_vector*)((char*)(obj) - XtOffsetOf(spl_vector, std)); @@ -166,7 +179,6 @@ static bool spl_vector_entries_uninitialized(const spl_vector_entries *array) static void spl_vector_raise_capacity(spl_vector *intern, const size_t new_capacity) { ZEND_ASSERT(new_capacity > intern->array.capacity); - ZEND_ASSERT(new_capacity <= MAX_VALID_OFFSET + 1); if (intern->array.capacity == 0) { intern->array.entries = safe_emalloc(new_capacity, sizeof(zval), 0); } else { @@ -205,82 +217,17 @@ static void spl_vector_entries_init_elems(spl_vector_entries *array, zend_long f } */ -static inline void spl_vector_entries_set_empty_list(spl_vector_entries *array) { +static zend_always_inline void spl_vector_entries_set_empty_list(spl_vector_entries *array) { array->size = 0; array->capacity = 0; array->entries = (zval *)empty_entry_list; } -static void spl_vector_entries_init_from_array(spl_vector_entries *array, zend_array *values, bool preserve_keys) -{ - const uint32_t num_elements = zend_hash_num_elements(values); - if (num_elements <= 0) { - spl_vector_entries_set_empty_list(array); - return; - } - - zval *val; - zval *entries; - - array->size = 0; /* reset size in case emalloc() fails */ - if (preserve_keys) { - zend_string *str_index; - zend_ulong num_index, max_index = 0; - - ZEND_HASH_FOREACH_KEY(values, num_index, str_index) { - if (UNEXPECTED(str_index != NULL || (zend_long)num_index < 0)) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "array must contain only positive integer keys"); - return; - } - - if (num_index > max_index) { - max_index = num_index; - } - } ZEND_HASH_FOREACH_END(); - - if (UNEXPECTED(max_index > MAX_VALID_OFFSET)) { - zend_error_noreturn("exceeded max valid offset of Vector"); - return; - } - const zend_ulong size = max_index + 1; - ZEND_ASSERT(size > 0); - array->entries = entries = safe_emalloc(size, sizeof(zval), 0); - array->size = size; - array->capacity = size; - /* Optimization: Don't need to null remaining elements if all elements from 0..num_elements-1 are set. */ - ZEND_ASSERT(size >= num_elements); - if (size > num_elements) { - for (uint32_t i = 0; i < size; i++) { - ZVAL_NULL(&entries[i]); - } - } - - ZEND_HASH_FOREACH_KEY_VAL(values, num_index, str_index, val) { - ZEND_ASSERT(num_index < size); - ZEND_ASSERT(!str_index); - /* should happen for non-corrupt array inputs */ - ZEND_ASSERT(size == num_elements || Z_TYPE(entries[num_index]) == IS_NULL); - ZVAL_COPY_DEREF(&entries[num_index], val); - } ZEND_HASH_FOREACH_END(); - return; - } - - size_t i = 0; - array->entries = entries = safe_emalloc(num_elements, sizeof(zval), 0); - array->size = num_elements; - array->capacity = num_elements; - ZEND_HASH_FOREACH_VAL(values, val) { - ZEND_ASSERT(i < num_elements); - ZVAL_COPY_DEREF(&entries[i], val); - i++; - } ZEND_HASH_FOREACH_END(); -} - static void spl_vector_entries_init_from_traversable(spl_vector_entries *array, zend_object *obj) { zend_class_entry *ce = obj->ce; zend_object_iterator *iter; - zend_long size = 0, capacity = 0; + size_t size = 0, capacity = 0; array->size = 0; array->entries = NULL; zval *entries = NULL; @@ -317,6 +264,7 @@ static void spl_vector_entries_init_from_traversable(spl_vector_entries *array, if (size >= capacity) { /* Not using Countable::count(), that would potentially have side effects or throw UnsupportedOperationException or be slow to compute */ if (entries) { + /* The safe_erealloc macro emits its own fatal error on integer overflow */ capacity *= 2; entries = safe_erealloc(entries, capacity, sizeof(zval), 0); } else { @@ -520,7 +468,7 @@ PHP_METHOD(Vector, clear) efree(old_entries); } -/* Set size of this vector */ +/* Set size of this Vector */ PHP_METHOD(Vector, setSize) { zend_long size; @@ -535,7 +483,8 @@ PHP_METHOD(Vector, setSize) if (size < 0) { zend_argument_value_error(1, "must be greater than or equal to 0"); } else { - zend_error_noreturn("exceeded max valid Teds\\Vector offset"); + spl_error_noreturn_max_vector_capacity(); + ZEND_UNREACHABLE(); } RETURN_THROWS(); } @@ -593,12 +542,10 @@ PHP_METHOD(Vector, __construct) { zval *object = ZEND_THIS; zval* iterable = NULL; - bool preserve_keys = true; - ZEND_PARSE_PARAMETERS_START(0, 2) + ZEND_PARSE_PARAMETERS_START(0, 1) Z_PARAM_OPTIONAL Z_PARAM_ITERABLE(iterable) - Z_PARAM_BOOL(preserve_keys) ZEND_PARSE_PARAMETERS_END(); spl_vector *intern = Z_VECTOR_P(object); @@ -608,17 +555,39 @@ PHP_METHOD(Vector, __construct) /* called __construct() twice, bail out */ RETURN_THROWS(); } + uint32_t num_elements; + HashTable *values; if (!iterable) { +set_empty_list: spl_vector_entries_set_empty_list(&intern->array); return; } switch (Z_TYPE_P(iterable)) { case IS_ARRAY: - spl_vector_entries_init_from_array(&intern->array, Z_ARRVAL_P(iterable), preserve_keys); + values = Z_ARRVAL_P(iterable); + num_elements = zend_hash_num_elements(values); + if (num_elements == 0) { + goto set_empty_list; + } + + zval *val; + zval *entries; + spl_vector_entries *array = &intern->array; + + array->size = 0; /* reset size in case emalloc() fails */ + size_t i = 0; + array->entries = entries = safe_emalloc(num_elements, sizeof(zval), 0); + array->size = num_elements; + array->capacity = num_elements; + ZEND_HASH_FOREACH_VAL(values, val) { + ZEND_ASSERT(i < num_elements); + ZVAL_COPY_DEREF(&entries[i], val); + i++; + } ZEND_HASH_FOREACH_END(); return; case IS_OBJECT: - spl_vector_entries_init_from_traversable(&intern->array, Z_OBJ_P(iterable), preserve_keys); + spl_vector_entries_init_from_traversable(&intern->array, Z_OBJ_P(iterable)); return; EMPTY_SWITCH_DEFAULT_CASE(); } @@ -776,9 +745,7 @@ static void spl_vector_entries_init_from_array_values(spl_vector_entries *array, { size_t num_entries = zend_hash_num_elements(raw_data); if (num_entries == 0) { - array->size = 0; - array->capacity = 0; - array->entries = NULL; + spl_vector_entries_set_empty_list(array); return; } zval *entries = safe_emalloc(num_entries, sizeof(zval), 0); @@ -1184,7 +1151,7 @@ static zend_always_inline void spl_vector_push(spl_vector *intern, zval *value) if (old_size >= old_capacity) { ZEND_ASSERT(old_size == old_capacity); - spl_vector_raise_capacity(intern, old_size ? old_size * 2 : 4); + spl_vector_raise_capacity(intern, old_size > 2 ? old_size * 2 : 4); } ZVAL_COPY(&intern->array.entries[old_size], value); intern->array.size++; @@ -1208,18 +1175,13 @@ PHP_METHOD(Vector, push) const size_t new_size = old_size + argc; /* The compiler will type check but eliminate dead code on platforms where size_t is 32 bits (4 bytes) */ if (SIZEOF_SIZE_T < 8 && UNEXPECTED(new_size > MAX_VALID_OFFSET + 1 || new_size < old_size)) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "exceeded max valid offset"); - RETURN_THROWS(); + spl_error_noreturn_max_vector_capacity(); + ZEND_UNREACHABLE(); } const size_t old_capacity = intern->array.capacity; if (new_size > old_capacity) { - size_t new_capacity = old_size ? old_size * 2 : 4; - if (UNEXPECTED(new_size > new_capacity)) { - new_capacity = new_size + (new_size >> 1); - } - if (SIZEOF_SIZE_T < 8 && UNEXPECTED(new_capacity > MAX_VALID_OFFSET + 1)) { - new_capacity = MAX_VALID_OFFSET + 1; - } + const size_t new_capacity = new_size >= 3 ? (new_size - 1) * 2 : 4; + ZEND_ASSERT(new_capacity >= new_size); spl_vector_raise_capacity(intern, new_capacity); } zval *entries = intern->array.entries; diff --git a/ext/spl/tests/Vector/Vector.phpt b/ext/spl/tests/Vector/Vector.phpt index 39566c8b27227..cec090ee0bc29 100644 --- a/ext/spl/tests/Vector/Vector.phpt +++ b/ext/spl/tests/Vector/Vector.phpt @@ -3,35 +3,25 @@ Vector constructed from array --FILE-- 'x', 'second' => new stdClass()], preserveKeys: false); -foreach ($it as $key => $value) { +$vec = new Vector(['first' => 'x', 'second' => new stdClass()]); +foreach ($vec as $key => $value) { printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); } -var_dump($it); -var_dump((array)$it); +var_dump($vec); +var_dump((array)$vec); -$it = new Vector([]); -var_dump($it); -var_dump((array)$it); -foreach ($it as $key => $value) { +$vec = new Vector([]); +var_dump($vec); +var_dump((array)$vec); +foreach ($vec as $key => $value) { echo "Unreachable\n"; } -// The default is to preserve keys -$it = new Vector([2 => 'third', 0 => 'first']); -var_dump($it); +// Vector will always reindex keys in the order of iteration, like array_values() does. +$vec = new Vector([2 => 'a', 0 => 'b']); +var_dump($vec); -try { - $it = new Vector([-1 => new stdClass()]); -} catch (UnexpectedValueException $e) { - echo "Caught: {$e->getMessage()}\n"; -} - -try { - $it = new Vector(['0a' => new stdClass()]); -} catch (UnexpectedValueException $e) { - echo "Caught: {$e->getMessage()}\n"; -} +var_dump(new Vector([-1 => new stdClass()])); ?> --EXPECT-- Key: 0 @@ -57,13 +47,14 @@ object(Vector)#3 (0) { } array(0) { } -object(Vector)#1 (3) { +object(Vector)#1 (2) { [0]=> - string(5) "first" + string(1) "a" [1]=> - NULL - [2]=> - string(5) "third" + string(1) "b" } -Caught: array must contain only positive integer keys -Caught: array must contain only positive integer keys \ No newline at end of file +object(Vector)#3 (1) { + [0]=> + object(stdClass)#4 (0) { + } +} \ No newline at end of file diff --git a/ext/spl/tests/Vector/aggregate.phpt b/ext/spl/tests/Vector/aggregate.phpt index 942c1d210974f..aa0cde1acac6b 100644 --- a/ext/spl/tests/Vector/aggregate.phpt +++ b/ext/spl/tests/Vector/aggregate.phpt @@ -3,15 +3,15 @@ Vector is an IteratorAggregate --FILE-- '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)); +$vector = new Vector(['x', (object)['key' => 'value']]); +foreach ($vector as $k => $v) { + foreach ($vector as $k2 => $v2) { + printf("k=%s k2=%s v=%s v2=%s\n", json_encode($k), json_encode($k2), json_encode($v), 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 +k=0 k2=0 v="x" v2="x" +k=0 k2=1 v="x" v2={"key":"value"} +k=1 k2=0 v={"key":"value"} v2="x" +k=1 k2=1 v={"key":"value"} v2={"key":"value"} \ No newline at end of file diff --git a/ext/spl/tests/Vector/arrayCast.phpt b/ext/spl/tests/Vector/arrayCast.phpt index befd62e298dd0..6dd1af66b369e 100644 --- a/ext/spl/tests/Vector/arrayCast.phpt +++ b/ext/spl/tests/Vector/arrayCast.phpt @@ -3,13 +3,13 @@ Vector to array --FILE-- pop(); -var_dump($it); +$vec = new Vector([strtoupper('test')]); +var_dump((array)$vec); +$vec[0] = strtoupper('test2'); +var_dump((array)$vec); +var_dump($vec); +$vec->pop(); +var_dump($vec); ?> diff --git a/ext/spl/tests/Vector/clear.phpt b/ext/spl/tests/Vector/clear.phpt index 82e0141b7dd4f..3164df2e66d88 100644 --- a/ext/spl/tests/Vector/clear.phpt +++ b/ext/spl/tests/Vector/clear.phpt @@ -3,17 +3,15 @@ Vector clear --FILE-- toArray()); -var_dump($it->count()); -var_dump($it->capacity()); -$it->clear(); -foreach ($it as $value) { +$vec = new Vector([new stdClass()]); +var_dump($vec->toArray()); +var_dump($vec->count()); +$vec->clear(); +foreach ($vec as $value) { echo "Not reached\n"; } -var_dump($it->toArray()); -var_dump($it->count()); -var_dump($it->capacity()); +var_dump($vec->toArray()); +var_dump($vec->count()); ?> --EXPECT-- array(1) { @@ -22,8 +20,6 @@ array(1) { } } int(1) -int(1) array(0) { } int(0) -int(0) diff --git a/ext/spl/tests/Vector/clone.phpt b/ext/spl/tests/Vector/clone.phpt index 35a215849e072..6106d34bbaadc 100644 --- a/ext/spl/tests/Vector/clone.phpt +++ b/ext/spl/tests/Vector/clone.phpt @@ -3,9 +3,9 @@ Vector can be cloned --FILE-- $value) { echo "Saw entry:\n"; var_dump($key, $value); diff --git a/ext/spl/tests/Vector/contains.phpt b/ext/spl/tests/Vector/contains.phpt index 1660af21ef450..3d9f4fb60e1a1 100644 --- a/ext/spl/tests/Vector/contains.phpt +++ b/ext/spl/tests/Vector/contains.phpt @@ -4,14 +4,14 @@ Vector contains()/indexOf(); contains($value)), json_encode($it->indexOf($value))); + printf("%s: contains=%s, indexOf=%s\n", json_encode($value), json_encode($vec->contains($value)), json_encode($vec->indexOf($value))); } ?> --EXPECT-- -null: contains=false, indexOf=false -"o": contains=false, indexOf=false +null: contains=false, indexOf=null +"o": contains=false, indexOf=null {}: contains=true, indexOf=1 -{}: contains=false, indexOf=false +{}: contains=false, indexOf=null "first": contains=true, indexOf=0 diff --git a/ext/spl/tests/Vector/exceptionhandler.phpt b/ext/spl/tests/Vector/exceptionhandler.phpt index b6fc1ff11fa3b..27cf372323798 100644 --- a/ext/spl/tests/Vector/exceptionhandler.phpt +++ b/ext/spl/tests/Vector/exceptionhandler.phpt @@ -21,7 +21,7 @@ function yields_and_throws() { echo "Unreachable\n"; } try { - $it = new Vector(yields_and_throws(), preserveKeys: false); + $vec = new Vector(yields_and_throws()); } catch (RuntimeException $e) { echo "Caught " . $e->getMessage() . "\n"; } diff --git a/ext/spl/tests/Vector/filter.phpt b/ext/spl/tests/Vector/filter.phpt index 379293b47158e..5fda5aae967d2 100644 --- a/ext/spl/tests/Vector/filter.phpt +++ b/ext/spl/tests/Vector/filter.phpt @@ -3,7 +3,7 @@ Vector filter() --FILE-- count(), $v->capacity()); + printf("count=%d\n", $v->count()); foreach ($v as $i => $value) { printf("Value #%d: %s\n", $i, var_export($value, true)); } @@ -31,25 +31,25 @@ try { } ?> --EXPECT-- -count=0 capacity=0 -count=1 capacity=1 +count=0 +count=1 Value #0: true -count=3 capacity=3 +count=3 Value #0: 'TEST' Value #1: true Value #2: (object) array( ) Test functions -count=0 capacity=0 -count=0 capacity=0 -count=2 capacity=2 +count=0 +count=0 +count=2 Value #0: false Value #1: array ( ) -count=0 capacity=0 -count=1 capacity=1 +count=0 +count=1 Value #0: true -count=3 capacity=3 +count=3 Value #0: 'TEST' Value #1: true Value #2: (object) array( diff --git a/ext/spl/tests/Vector/isset.phpt b/ext/spl/tests/Vector/isset.phpt new file mode 100644 index 0000000000000..3d020ba3bfee5 --- /dev/null +++ b/ext/spl/tests/Vector/isset.phpt @@ -0,0 +1,21 @@ +--TEST-- +Vector isset +--FILE-- + +--EXPECTF-- +offset=0 isset=false empty=true value=null +offset=1 isset=true empty=true value=false +offset=2 isset=true empty=false value={} +offset=3 isset=false empty=true value=null +offset=-1 isset=false empty=true value=null +offset="1" isset=true empty=true value=false +offset=-%d isset=false empty=true value=null +offset=%d isset=false empty=true value=null +offset=1 isset=true empty=true value=false +offset=false isset=false empty=true value=null +offset=true isset=true empty=true value=false diff --git a/ext/spl/tests/Vector/map.phpt b/ext/spl/tests/Vector/map.phpt index 486dfca80feae..1e3848a6328c3 100644 --- a/ext/spl/tests/Vector/map.phpt +++ b/ext/spl/tests/Vector/map.phpt @@ -3,7 +3,7 @@ Vector map() --FILE-- count(), $v->capacity()); + printf("count=%d\n", $v->count()); foreach ($v as $i => $value) { printf("Value #%d: %s\n", $i, json_encode($value)); } @@ -18,21 +18,21 @@ $false = fn() => false; dump_vector((new Vector([strtoupper('test'), true, false, new stdClass(), []]))->map($false)); ?> --EXPECT-- -count=0 capacity=0 -count=5 capacity=5 +count=0 +count=5 Value #0: ["TEST"] Value #1: [true] Value #2: [false] Value #3: [{}] Value #4: [[]] -count=0 capacity=0 -count=5 capacity=5 +count=0 +count=5 Value #0: "TEST" Value #1: true Value #2: false Value #3: {} Value #4: [] -count=5 capacity=5 +count=5 Value #0: false Value #1: false Value #2: false diff --git a/ext/spl/tests/Vector/offsetGet.phpt b/ext/spl/tests/Vector/offsetGet.phpt index fec7da758e2ce..3250e2bd7e9f2 100644 --- a/ext/spl/tests/Vector/offsetGet.phpt +++ b/ext/spl/tests/Vector/offsetGet.phpt @@ -1,5 +1,5 @@ --TEST-- -Vector offsetGet/valueAt +Vector offsetGet/get --FILE-- (new ReflectionClass(Vector::class))->newInstanceWithoutConstructor()); -$it = new Vector([new stdClass()]); -var_dump($it->offsetGet(0)); -var_dump($it->valueAt(0)); -expect_throws(fn() => $it->offsetSet(1,'x')); -expect_throws(fn() => $it->offsetUnset(0)); -var_dump($it->offsetGet('0')); +$vec = new Vector([new stdClass()]); +var_dump($vec->offsetGet(0)); +var_dump($vec->get(0)); +expect_throws(fn() => $vec->offsetSet(1,'x')); +expect_throws(fn() => $vec->offsetUnset(0)); +var_dump($vec->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->valueAt(1)); -expect_throws(fn() => $it->valueAt(-1)); +var_dump($vec->offsetExists(1)); +var_dump($vec->offsetExists('1')); +var_dump($vec->offsetExists(PHP_INT_MAX)); +var_dump($vec->offsetExists(PHP_INT_MIN)); +expect_throws(fn() => $vec->get(1)); +expect_throws(fn() => $vec->get(-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->valueAt(PHP_INT_MAX)); -expect_throws(fn() => $it->valueAt(PHP_INT_MIN)); -expect_throws(fn() => $it->valueAt(1)); -expect_throws(fn() => $it->valueAt(-1)); -expect_throws(fn() => $it->offsetGet(1)); -expect_throws(fn() => $it->offsetGet('1')); -expect_throws(fn() => $it->offsetGet('invalid')); -expect_throws(fn() => $it->valueAt('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[0]); }); -var_dump($it->getIterator()); +expect_throws(fn() => $vec->offsetGet(PHP_INT_MAX)); +expect_throws(fn() => $vec->offsetGet(PHP_INT_MIN)); +expect_throws(fn() => $vec->offsetGet(1)); +expect_throws(fn() => $vec->get(PHP_INT_MAX)); +expect_throws(fn() => $vec->get(PHP_INT_MIN)); +expect_throws(fn() => $vec->get(1)); +expect_throws(fn() => $vec->get(-1)); +expect_throws(fn() => $vec->offsetGet(1)); +expect_throws(fn() => $vec->offsetGet('1')); +expect_throws(fn() => $vec->offsetGet('invalid')); +expect_throws(fn() => $vec->get('invalid')); +expect_throws(fn() => $vec[['invalid']]); +expect_throws(fn() => $vec->offsetUnset(PHP_INT_MAX)); +expect_throws(fn() => $vec->offsetSet(PHP_INT_MAX,'x')); +expect_throws(function () use ($vec) { unset($vec[0]); }); +var_dump($vec->getIterator()); ?> --EXPECT-- Caught ReflectionException: Class Vector is an internal class marked as final that cannot be instantiated without invoking its constructor @@ -71,7 +71,7 @@ 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: Vector::valueAt(): Argument #1 ($offset) must be of type int, string given +Caught TypeError: Vector::get(): Argument #1 ($offset) must be of type int, string given Caught TypeError: Illegal offset type Caught RuntimeException: Vector does not support offsetUnset - elements must be set to null or removed by resizing Caught OutOfBoundsException: Index out of range diff --git a/ext/spl/tests/Vector/push_pop.phpt b/ext/spl/tests/Vector/push_pop.phpt index 9196d56d69806..f8abd46f33511 100644 --- a/ext/spl/tests/Vector/push_pop.phpt +++ b/ext/spl/tests/Vector/push_pop.phpt @@ -1,5 +1,5 @@ --TEST-- -Vector push/pop +Vector push(...$args)/pop --FILE-- count(), $it->capacity()); -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[] = null; -echo json_encode($it), "\n"; -printf("count=%d capacity=%d\n", count($it), $it->capacity()); -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 capacity=%d\n", count($it), $it->capacity()); +$vec = new Vector([]); +printf("count=%d\n", $vec->count()); +expect_throws(fn() => $vec->pop()); +expect_throws(fn() => $vec->pop()); +$vec->push(strtoupper('test')); +$vec->push(['literal']); +$vec->push(new stdClass()); +$vec[] = strtoupper('test2'); +$vec[] = null; +echo json_encode($vec), "\n"; +printf("count=%d\n", count($vec)); +var_dump($vec->pop()); +var_dump($vec->pop()); +var_dump($vec->pop()); +var_dump($vec->pop()); +echo "After popping 4 elements: ", json_encode($vec->toArray()), "\n"; +var_dump($vec->pop()); +echo json_encode($vec), "\n"; +printf("count=%d\n", count($vec)); echo "After shrinkToFit\n"; -$it->shrinkToFit(); -echo json_encode($it), "\n"; -printf("count=%d capacity=%d\n", count($it), $it->capacity()); +$vec->shrinkToFit(); +echo json_encode($vec), "\n"; +printf("count=%d\n", count($vec)); +echo "After pushing variadic args\n"; +$vec->push(strtoupper('test'), 'other', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); +echo json_encode($vec), "\n"; +printf("count=%d\n", count($vec)); +$vec->push('a', 'b'); +echo json_encode($vec), "\n"; +// After pushing no args (like array_push, e.g. for variadic argument lists) +$vec->push(); +echo json_encode($vec), "\n"; ?> --EXPECT-- Test empty vector -count=0 capacity=0 +count=0 Caught UnderflowException: Cannot pop from empty Vector Caught UnderflowException: Cannot pop from empty Vector ["TEST",["literal"],{},"TEST2",null] -count=5 capacity=8 +count=5 NULL string(5) "TEST2" object(stdClass)#2 (0) { @@ -56,7 +65,12 @@ array(1) { After popping 4 elements: ["TEST"] string(4) "TEST" [] -count=0 capacity=4 +count=0 After shrinkToFit [] -count=0 capacity=0 \ No newline at end of file +count=0 +After pushing variadic args +["TEST","other",1,2,3,4,5,6,7,8,9,10] +count=12 +["TEST","other",1,2,3,4,5,6,7,8,9,10,"a","b"] +["TEST","other",1,2,3,4,5,6,7,8,9,10,"a","b"] \ No newline at end of file diff --git a/ext/spl/tests/Vector/reinit_forbidden.phpt b/ext/spl/tests/Vector/reinit_forbidden.phpt index df82a4389058a..694dcf1f0fefe 100644 --- a/ext/spl/tests/Vector/reinit_forbidden.phpt +++ b/ext/spl/tests/Vector/reinit_forbidden.phpt @@ -3,22 +3,22 @@ Vector cannot be re-initialized --FILE-- __construct(['first']); + $vec->__construct(['first']); echo "Unexpectedly called constructor\n"; } catch (Throwable $t) { printf("Caught %s: %s\n", $t::class, $t->getMessage()); } -var_dump($it); +var_dump($vec); try { - $it->__unserialize([new ArrayObject(), new stdClass()]); + $vec->__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); +var_dump($vec); ?> --EXPECT-- Caught RuntimeException: Called Vector::__construct twice diff --git a/ext/spl/tests/Vector/serialization.phpt b/ext/spl/tests/Vector/serialization.phpt index de107f1cbd026..af300972901e9 100644 --- a/ext/spl/tests/Vector/serialization.phpt +++ b/ext/spl/tests/Vector/serialization.phpt @@ -3,23 +3,23 @@ Vector can be serialized and unserialized --FILE-- new stdClass()], preserveKeys: false); +$vec = new Vector([new stdClass()]); try { - $it->dynamicProp = 123; + $vec->dynamicProp = 123; } catch (Throwable $t) { printf("Caught %s: %s\n", $t::class, $t->getMessage()); } -$ser = serialize($it); +$ser = serialize($vec); echo $ser, "\n"; foreach (unserialize($ser) as $key => $value) { echo "Entry:\n"; var_dump($key, $value); } -var_dump($ser === serialize($it)); +var_dump($ser === serialize($vec)); echo "Done\n"; $x = 123; -$it = new Vector([]); -var_dump($it->__serialize()); +$vec = new Vector([]); +var_dump($vec->__serialize()); ?> --EXPECT-- Caught Error: Cannot create dynamic property Vector::$dynamicProp diff --git a/ext/spl/tests/Vector/setSize.phpt b/ext/spl/tests/Vector/setSize.phpt index b0f8f15f2d90f..6b9e5e5341c8a 100644 --- a/ext/spl/tests/Vector/setSize.phpt +++ b/ext/spl/tests/Vector/setSize.phpt @@ -2,24 +2,24 @@ Vector setSize --FILE-- count(), $it->capacity()); - var_dump($it->toArray()); +function show(Vector $vec) { + printf("count=%d\n", $vec->count()); + var_dump($vec->toArray()); } -$it = new Vector(); -show($it); -$it->setSize(2); -$it[0] = new stdClass(); -show($it); -$it->setSize(0); -show($it); +$vec = new Vector(); +show($vec); +$vec->setSize(2); +$vec[0] = new stdClass(); +show($vec); +$vec->setSize(0); +show($vec); ?> --EXPECT-- -count=0 capacity=0 +count=0 array(0) { } -count=2 capacity=2 +count=2 array(2) { [0]=> object(stdClass)#2 (0) { @@ -27,6 +27,6 @@ array(2) { [1]=> NULL } -count=0 capacity=0 +count=0 array(0) { } diff --git a/ext/spl/tests/Vector/setSize_default.phpt b/ext/spl/tests/Vector/setSize_default.phpt new file mode 100644 index 0000000000000..30dcb04f6195c --- /dev/null +++ b/ext/spl/tests/Vector/setSize_default.phpt @@ -0,0 +1,24 @@ +--TEST-- +Vector setSize with $default +--FILE-- +count()); +} + +$vec = new Vector(); +show($vec); +$vec->setSize(2, 0); +$vec[0] = new stdClass(); +$vec->setSize(3, null); +show($vec); +$vec->setSize(0, new stdClass()); +show($vec); +$vec->setSize(5, strtoupper('na')); +show($vec); +?> +--EXPECT-- +value=[] count=0 +value=[{},0,null] count=3 +value=[] count=0 +value=["NA","NA","NA","NA","NA"] count=5 diff --git a/ext/spl/tests/Vector/setValueAt.phpt b/ext/spl/tests/Vector/setValueAt.phpt index e2bf69afb759b..9c62a5608594d 100644 --- a/ext/spl/tests/Vector/setValueAt.phpt +++ b/ext/spl/tests/Vector/setValueAt.phpt @@ -1,5 +1,5 @@ --TEST-- -Vector offsetSet/setValueAt +Vector offsetSet/set --FILE-- $it->offsetSet(0, strtoupper('value'))); -expect_throws(fn() => $it->setValueAt(0, strtoupper('value'))); +$vec = new Vector([]); +expect_throws(fn() => $vec->offsetSet(0, strtoupper('value'))); +expect_throws(fn() => $vec->set(0, strtoupper('value'))); echo "Test short vector\n"; $str = 'Test short vector'; -$it = new Vector(explode(' ', $str)); -$it->setValueAt(0, 'new'); -$it->offsetSet(2, strtoupper('test')); -echo json_encode($it), "\n"; -expect_throws(fn() => $it->setValueAt(-1, strtoupper('value'))); -expect_throws(fn() => $it->setValueAt(3, 'end')); -expect_throws(fn() => $it->setValueAt(PHP_INT_MAX, 'end')); +$vec = new Vector(explode(' ', $str)); +$vec->set(0, 'new'); +$vec->offsetSet(2, strtoupper('test')); +echo json_encode($vec), "\n"; +expect_throws(fn() => $vec->set(-1, strtoupper('value'))); +expect_throws(fn() => $vec->set(3, 'end')); +expect_throws(fn() => $vec->set(PHP_INT_MAX, 'end')); ?> --EXPECT-- diff --git a/ext/spl/tests/Vector/set_state.phpt b/ext/spl/tests/Vector/set_state.phpt index 727067ce3a43f..7150edb2a4b97 100644 --- a/ext/spl/tests/Vector/set_state.phpt +++ b/ext/spl/tests/Vector/set_state.phpt @@ -8,13 +8,13 @@ function dump_repr($obj) { } dump_repr(Vector::__set_state([])); Vector::__set_state(['first' => 'x']); -$it = Vector::__set_state([strtoupper('a literal'), ['first', 'x'], [(object)['key' => 'value'], null]]); -foreach ($it as $key => $value) { +$vec = Vector::__set_state([strtoupper('a literal'), ['first', 'x'], [(object)['key' => 'value'], null]]); +foreach ($vec 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); +dump_repr($vec); +var_dump($vec); +var_dump((array)$vec); ?> --EXPECT-- diff --git a/ext/spl/tests/Vector/shrink_capacity.phpt b/ext/spl/tests/Vector/shrink_capacity.phpt index c7c1e78c13c53..4d2cb86f0b499 100644 --- a/ext/spl/tests/Vector/shrink_capacity.phpt +++ b/ext/spl/tests/Vector/shrink_capacity.phpt @@ -1,40 +1,40 @@ --TEST-- -Vector pop() shrinks capacity +Vector pop() reduces count --FILE-- 0) { - var_dump($it->pop()); - printf("capacity=%d\n", $it->capacity()); +$vec = new Vector(explode(' ', $s)); +while (count($vec) > 0) { + var_dump($vec->pop()); + printf("new count=%d\n", count($vec)); } -$it->shrinkToFit(); -printf("shrinkToFit capacity=%d\n", $it->capacity()); -printf("shrinkToFit capacity=%d\n", $it->capacity()); -$it->push(new stdClass()); -printf("it=%s count=%d capacity=%d\n", json_encode($it), count($it), $it->capacity()); -$it->shrinkToFit(); -printf("it=%s count=%d capacity=%d\n", json_encode($it), count($it), $it->capacity()); +$vec->shrinkToFit(); +printf("shrinkToFit\n"); +printf("shrinkToFit\n"); +$vec->push(new stdClass()); +printf("it=%s count=%d\n", json_encode($vec), count($vec)); +$vec->shrinkToFit(); +printf("it=%s count=%d\n", json_encode($vec), count($vec)); ?> --EXPECT-- string(4) "test" -capacity=8 +new count=7 string(4) "test" -capacity=8 +new count=6 string(4) "test" -capacity=8 +new count=5 string(4) "test" -capacity=8 +new count=4 string(4) "test" -capacity=8 +new count=3 string(4) "test" -capacity=8 +new count=2 string(3) "123" -capacity=8 +new count=1 string(4) "test" -capacity=4 -shrinkToFit capacity=0 -shrinkToFit capacity=0 -it=[{}] count=1 capacity=4 -it=[{}] count=1 capacity=1 +new count=0 +shrinkToFit +shrinkToFit +it=[{}] count=1 +it=[{}] count=1 \ No newline at end of file diff --git a/ext/spl/tests/Vector/toArray.phpt b/ext/spl/tests/Vector/toArray.phpt index 47a4af3cfea10..392ca6cdbb04d 100644 --- a/ext/spl/tests/Vector/toArray.phpt +++ b/ext/spl/tests/Vector/toArray.phpt @@ -3,12 +3,17 @@ Vector toArray() --FILE-- new stdClass()], false); -var_dump($it->toArray()); -var_dump($it->toArray()); -$it = new Vector([]); -var_dump($it->toArray()); -var_dump($it->toArray()); +$vec = new Vector([new stdClass()]); +var_dump($vec->toArray()); +var_dump($vec->toArray()); +$vec = new Vector([]); +var_dump($vec->toArray()); +var_dump($vec->toArray()); +$vec->setSize(2, strtoupper('test')); +var_dump($vec->toArray()); +var_dump($vec->toArray()); +var_dump(Vector::__set_state([])->toArray()); +var_dump(Vector::__set_state([(object)[]])->toArray()); ?> --EXPECT-- array(1) { @@ -24,4 +29,23 @@ array(1) { array(0) { } array(0) { +} +array(2) { + [0]=> + string(4) "TEST" + [1]=> + string(4) "TEST" +} +array(2) { + [0]=> + string(4) "TEST" + [1]=> + string(4) "TEST" +} +array(0) { +} +array(1) { + [0]=> + object(stdClass)#1 (0) { + } } \ No newline at end of file diff --git a/ext/spl/tests/Vector/traversable.phpt b/ext/spl/tests/Vector/traversable.phpt index c516905babd0b..7fa78e66fabe4 100644 --- a/ext/spl/tests/Vector/traversable.phpt +++ b/ext/spl/tests/Vector/traversable.phpt @@ -15,28 +15,26 @@ function yields_values() { } // Vector eagerly evaluates the passed in Traversable -$it = new Vector(yields_values(), preserveKeys: false); -foreach ($it as $key => $value) { +$vec = new Vector(yields_values()); +foreach ($vec 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) { +foreach ($vec as $key => $value) { printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); } -unset($it); +unset($vec); -foreach ([false, true] as $preserveKeys) { - $emptyIt = new Vector(new ArrayObject(), $preserveKeys); - var_dump($emptyIt); - foreach ($emptyIt as $key => $value) { - echo "Unreachable\n"; - } - foreach ($emptyIt as $key => $value) { - echo "Unreachable\n"; - } - echo "Done\n"; - unset($emptyIt); +$emptyIt = new Vector(new ArrayObject()); +var_dump($emptyIt); +foreach ($emptyIt as $key => $value) { + echo "Unreachable\n"; +} +foreach ($emptyIt as $key => $value) { + echo "Unreachable\n"; } +echo "Done\n"; +unset($emptyIt); ?> --EXPECT-- @@ -100,7 +98,4 @@ Key: 12 Value: 2 object(Vector)#1 (0) { } -Done -object(Vector)#1 (0) { -} Done \ No newline at end of file diff --git a/ext/spl/tests/Vector/traversable_preserve_keys.phpt b/ext/spl/tests/Vector/traversable_preserve_keys.phpt deleted file mode 100644 index e6dac37ef082e..0000000000000 --- a/ext/spl/tests/Vector/traversable_preserve_keys.phpt +++ /dev/null @@ -1,48 +0,0 @@ ---TEST-- -Vector constructed from Traversable preserves keys ---FILE-- - (object)['key' => 'value']; - yield 0 => new stdClass();; - yield 0 => true; -} - -function yields_bad_value() { - yield 5 => (object)['key' => 'value']; - yield -1 => new stdClass(); -} - -// Vector eagerly evaluates the passed in Traversable -$it = new Vector(yields_values()); -foreach ($it as $key => $value) { - printf("Key: %s\nValue: %s\n", var_export($key, true), var_export($value, true)); -} -printf("count=%d capacity=%d\n", $it->count(), $it->capacity()); -unset($it); - -try { - $it = new Vector(yields_bad_value()); -} catch (Exception $e) { - printf("Caught %s: %s\n", get_class($e), $e->getMessage()); -} - -?> ---EXPECT-- -Key: 0 -Value: true -Key: 1 -Value: NULL -Key: 2 -Value: NULL -Key: 3 -Value: NULL -Key: 4 -Value: NULL -Key: 5 -Value: (object) array( - 'key' => 'value', -) -count=6 capacity=6 -Caught UnexpectedValueException: array must contain only positive integer keys \ No newline at end of file