From c9e6b9b4ee2a86decba695ad531537cb8c6e0d31 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 21 Dec 2021 12:27:47 -0500 Subject: [PATCH 1/2] Add ARRAY_UNIQUE_IDENTICAL option --- Zend/zend_portability.h | 53 ++++ Zend/zend_stricthash.c | 185 ++++++++++++++ Zend/zend_stricthash.h | 44 ++++ Zend/zend_strictmap.c | 241 ++++++++++++++++++ Zend/zend_strictmap.h | 84 ++++++ bench.php | 35 +++ configure.ac | 1 + ext/standard/array.c | 37 +++ ext/standard/basic_functions.stub.php | 6 + ext/standard/basic_functions_arginfo.h | 3 +- ext/standard/php_array.h | 4 + .../tests/array/array_unique_identical.phpt | 77 ++++++ .../array/array_unique_identical_enums.phpt | 83 ++++++ .../array/array_unique_identical_nan.phpt | 77 ++++++ .../array_unique_identical_recursion.phpt | 13 + win32/build/config.w32 | 2 +- 16 files changed, 943 insertions(+), 2 deletions(-) create mode 100644 Zend/zend_stricthash.c create mode 100644 Zend/zend_stricthash.h create mode 100644 Zend/zend_strictmap.c create mode 100644 Zend/zend_strictmap.h create mode 100644 bench.php create mode 100644 ext/standard/tests/array/array_unique_identical.phpt create mode 100644 ext/standard/tests/array/array_unique_identical_enums.phpt create mode 100644 ext/standard/tests/array/array_unique_identical_nan.phpt create mode 100644 ext/standard/tests/array/array_unique_identical_recursion.phpt diff --git a/Zend/zend_portability.h b/Zend/zend_portability.h index 7fdeedee19bf4..7aa931e6a81b4 100644 --- a/Zend/zend_portability.h +++ b/Zend/zend_portability.h @@ -701,4 +701,57 @@ extern "C++" { # define ZEND_INDIRECT_RETURN #endif +// bswap compiler checks copied from https://github.com/google/cityhash/blob/8af9b8c2b889d80c22d6bc26ba0df1afb79a30db/src/city.cc#L50 +// +// Copyright (c) 2011 Google, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +#ifdef _MSC_VER +# include +# define ZEND_BSWAP_32(x) _byteswap_ulong(x) +# define ZEND_BSWAP_64(x) _byteswap_uint64(x) +#elif defined(__APPLE__) +// Mac OS X / Darwin features +# include +# define ZEND_BSWAP_32(x) OSSwapInt32(x) +# define ZEND_BSWAP_64(x) OSSwapInt64(x) +#elif defined(__sun) || defined(sun) +# include +# define ZEND_BSWAP_32(x) BSWAP_32(x) +# define ZEND_BSWAP_64(x) BSWAP_64(x) +#elif defined(__FreeBSD__) +# include +# define ZEND_BSWAP_32(x) bswap32(x) +# define ZEND_BSWAP_64(x) bswap64(x) +#elif defined(__OpenBSD__) +# include +# define ZEND_BSWAP_32(x) swap32(x) +# define ZEND_BSWAP_64(x) swap64(x) +#elif defined(__NetBSD__) +# include +# include +# define ZEND_BSWAP_32(x) bswap32(x) +# define ZEND_BSWAP_64(x) bswap64(x) +#else +# include +# define ZEND_BSWAP_32(x) bswap_32(x) +# define ZEND_BSWAP_64(x) bswap_64(x) +#endif + #endif /* ZEND_PORTABILITY_H */ diff --git a/Zend/zend_stricthash.c b/Zend/zend_stricthash.c new file mode 100644 index 0000000000000..5e61cfcf6bfec --- /dev/null +++ b/Zend/zend_stricthash.c @@ -0,0 +1,185 @@ +/* + Copyright (c) 2021 Tyson Andre + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of the 'teds' nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "php.h" +#include "php_ini.h" + +#include "Zend/zend_strictmap.h" + +#define ZEND_STRICTHASH_HASH_NULL 8310 +#define ZEND_STRICTHASH_HASH_FALSE 8311 +#define ZEND_STRICTHASH_HASH_TRUE 8312 +#define ZEND_STRICTHASH_HASH_EMPTY_ARRAY 8313 +/* + * See https://en.wikipedia.org/wiki/NaN + * For nan, the 12 most significant bits are: + * - 1 sign bit (0 or 1) + * - 11 sign bits + * (and at least one of the significand bits must be non-zero) + * + * Here, 0xff is the most significant byte with the sign and part of the exponent, + * and 0xf8 is the second most significant byte with part of the exponent and significand. + * + * Return an arbitrary choice of 0xff f_, with bytes in the reverse order. + */ +#define ZEND_STRICTHASH_HASH_NAN 0xf8ff + +#define ZEND_STRICTHASH_HASH_OFFSET_DOUBLE 8315 +#define ZEND_STRICTHASH_HASH_OFFSET_OBJECT 31415926 +#define ZEND_STRICTHASH_HASH_OFFSET_RESOURCE 27182818 + +typedef struct _array_rec_prot_node { + const zend_array *ht; + const struct _array_rec_prot_node *prev; +} array_rec_prot_node; + +static zend_long zend_stricthash_array(HashTable *const ht, const array_rec_prot_node *const node); +static uint64_t zend_convert_double_to_uint64_t(double value); +static zend_always_inline zend_long zend_stricthash_inner(zval *value, array_rec_prot_node *node); + +static zend_always_inline uint64_t zend_inline_hash_of_uint64(uint64_t orig) { + /* Copied from code written for igbinary. Works best when data that frequently + * differs is in the least significant bits of data. */ + uint64_t data = orig * 0x5e2d58d8b3bce8d9; + return ZEND_BSWAP_64(data); +} + +zend_long zend_stricthash_hash(zval *value) { + uint64_t raw_data = zend_stricthash_inner(value, NULL); + return zend_inline_hash_of_uint64(raw_data); +} + +static zend_always_inline zend_long zend_stricthash_inner(zval *value, array_rec_prot_node *node) { +again: + switch (Z_TYPE_P(value)) { + case IS_NULL: + return ZEND_STRICTHASH_HASH_NULL; + case IS_FALSE: + return ZEND_STRICTHASH_HASH_FALSE; + case IS_TRUE: + return ZEND_STRICTHASH_HASH_TRUE; + case IS_LONG: + return Z_LVAL_P(value); + case IS_DOUBLE: + return zend_convert_double_to_uint64_t(Z_DVAL_P(value)) + ZEND_STRICTHASH_HASH_OFFSET_DOUBLE; + case IS_STRING: + return ZSTR_HASH(Z_STR_P(value)); + case IS_ARRAY: + return zend_stricthash_array(Z_ARR_P(value), node); + case IS_OBJECT: + return Z_OBJ_HANDLE_P(value) + ZEND_STRICTHASH_HASH_OFFSET_OBJECT; + case IS_RESOURCE: + return Z_RES_HANDLE_P(value) + ZEND_STRICTHASH_HASH_OFFSET_RESOURCE; + case IS_REFERENCE: + value = Z_REFVAL_P(value); + goto again; + case IS_INDIRECT: + value = Z_INDIRECT_P(value); + goto again; + EMPTY_SWITCH_DEFAULT_CASE(); + } +} + +inline static uint64_t zend_convert_double_to_uint64_t(double value) { + if (value == 0) { + /* Signed positive and negative 0 have different bits. However, $signedZero === $signedNegativeZero in php and many other languages. */ + return 0; + } + if (UNEXPECTED(isnan(value))) { + return ZEND_STRICTHASH_HASH_NAN; + } + uint8_t *data = (uint8_t *)&value; +#ifndef WORDS_BIGENDIAN + return + (((uint64_t)data[0]) << 56) | + (((uint64_t)data[1]) << 48) | + (((uint64_t)data[2]) << 40) | + (((uint64_t)data[3]) << 32) | + (((uint64_t)data[4]) << 24) | + (((uint64_t)data[5]) << 16) | + (((uint64_t)data[6]) << 8) | + (((uint64_t)data[7])); +#else + return + (((uint64_t)data[7]) << 56) | + (((uint64_t)data[6]) << 48) | + (((uint64_t)data[5]) << 40) | + (((uint64_t)data[4]) << 32) | + (((uint64_t)data[3]) << 24) | + (((uint64_t)data[2]) << 16) | + (((uint64_t)data[1]) << 8) | + (((uint64_t)data[0])); +#endif +} + +static zend_long zend_stricthash_array(HashTable *const ht, const array_rec_prot_node *const node) { + if (zend_hash_num_elements(ht) == 0) { + return ZEND_STRICTHASH_HASH_EMPTY_ARRAY; + } + + uint64_t result = 1; + bool protected_recursion = false; + + array_rec_prot_node new_node; + array_rec_prot_node *new_node_ptr = NULL; + if (!(GC_FLAGS(ht) & GC_IMMUTABLE)) { + new_node.prev = node; + new_node.ht = ht; + if (UNEXPECTED(GC_IS_RECURSIVE(ht))) { + for (const array_rec_prot_node *tmp = node; tmp != NULL; tmp = tmp->prev) { + if (tmp->ht == ht) { + zend_error_noreturn(E_ERROR, "Nesting level too deep - recursive dependency?"); + } + } + } else { + protected_recursion = true; + GC_PROTECT_RECURSION(ht); + } + new_node_ptr = &new_node; + } + + zend_long num_key; + zend_string *str_key; + zval *field_value; + ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, str_key, field_value) { + /* str_key is in a hash table, meaning that the hash was already computed. */ + result += str_key ? ZSTR_H(str_key) : (zend_ulong) num_key; + zend_long field_hash = zend_stricthash_inner(field_value, new_node_ptr); + result += (field_hash + (result << 7)); + result = zend_inline_hash_of_uint64(result); + } ZEND_HASH_FOREACH_END(); + + if (protected_recursion) { + GC_UNPROTECT_RECURSION(ht); + } + return result; +} diff --git a/Zend/zend_stricthash.h b/Zend/zend_stricthash.h new file mode 100644 index 0000000000000..8e72c5234db4b --- /dev/null +++ b/Zend/zend_stricthash.h @@ -0,0 +1,44 @@ +/* + Copyright (c) 2021 Tyson Andre + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of the 'teds' nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ZEND_STRICTHASH_H +#define ZEND_STRICTHASH_H + +#include "Zend/zend_types.h" + +zend_long zend_stricthash_hash(zval *value); + +static zend_always_inline uint32_t zend_stricthash_hash_uint32_t(zval *value) { + return (uint32_t) (zend_ulong) zend_stricthash_hash(value); +} + +#endif /* ZEND_STRICTHASH_H */ diff --git a/Zend/zend_strictmap.c b/Zend/zend_strictmap.c new file mode 100644 index 0000000000000..867a91ef0e2cb --- /dev/null +++ b/Zend/zend_strictmap.c @@ -0,0 +1,241 @@ +/* + Copyright (c) 2021 Tyson Andre + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of the 'teds' nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "php.h" +#include "php_ini.h" + +#include "Zend/zend_strictmap.h" +#include "Zend/zend_stricthash.h" + +#define SM_MIN_CAPACITY 8 +#define SM_IT_HASH(entry) Z_EXTRA((entry)->key) +#define SM_IT_NEXT(entry) Z_NEXT((entry)->value) +#define SM_SIZE_TO_MASK(capacity) ((uint32_t)(-(capacity))); +#define SM_MEMORY_PER_ENTRY (sizeof(zend_strictmap_entry) + sizeof(uint32_t)) +#define SM_INVALID_INDEX ((uint32_t) -1) +#define SM_HASH_EX(data, idx) ((uint32_t*)(data))[(int32_t)(idx)] +#define SM_HASH(ht, idx) SM_HASH_EX((ht)->data, idx) + +static size_t zend_strictmap_offset_bytes_for_capacity(uint32_t capacity); +static void zend_strictmap_grow(zend_strictmap *array); +static zend_strictmap_entry *zend_strictmap_find_entry(const zend_strictmap *ht, zval *key, const uint32_t h); +static zend_strictmap_entry *zend_strictmap_alloc_entries(uint32_t capacity); + +void zend_strictmap_init(zend_strictmap *array) +{ + array->count = 0; + array->used_indices = 0; + array->first_used_index = 0; + array->data = zend_strictmap_alloc_entries(SM_MIN_CAPACITY); + array->capacity = SM_MIN_CAPACITY; + array->hash_mask = SM_SIZE_TO_MASK(SM_MIN_CAPACITY); + array->free_index_chain = SM_INVALID_INDEX; +} + +void zend_strictmap_dtor(zend_strictmap *array) +{ + zend_strictmap_entry *entries = array->data; + uint32_t capacity = array->capacity; + uint8_t *data = ((uint8_t *)entries) - zend_strictmap_offset_bytes_for_capacity(capacity); + efree(data); +} + +/* Returns true if a new entry was added to the map, false if updated. Based on _zend_hash_add_or_update_i. */ +bool zend_strictmap_insert(zend_strictmap *array, zval *key, zval *value) +{ + ZEND_ASSERT(Z_TYPE_P(key) != IS_UNDEF); + ZEND_ASSERT(Z_TYPE_P(value) != IS_UNDEF); + + const uint32_t h = zend_stricthash_hash_uint32_t(key); + + zend_strictmap_entry *p = zend_strictmap_find_entry(array, key, h); + if (p) { + ZVAL_COPY_VALUE(&p->value, value); + return false; + } + + /* If the Hash table is full, resize it */ + if (UNEXPECTED(array->free_index_chain == SM_INVALID_INDEX && array->used_indices >= array->capacity)) { + zend_strictmap_grow(array); + } + + zend_strictmap_entry *const data = array->data; + uint32_t idx; + if (array->free_index_chain == SM_INVALID_INDEX) { + idx = array->used_indices; + array->used_indices++; + } else { + idx = array->free_index_chain; + zend_strictmap_entry *const new_entry = data + idx; + array->free_index_chain = SM_IT_NEXT(new_entry); + } + p = data + idx; + const uint32_t nIndex = h | array->hash_mask; + array->count++; + SM_IT_HASH(p) = h; + SM_IT_NEXT(p) = SM_HASH_EX(data, nIndex); + SM_HASH_EX(data, nIndex) = idx; + + ZVAL_COPY_VALUE(&p->key, key); + ZVAL_COPY_VALUE(&p->value, value); + + return true; +} + +// Unused and untested +// bool zend_strictmap_remove_key(zend_strictmap *array, zval *key) +// { +// if (array->count == 0) { +// return false; +// } +// zend_strictmap_entry *const entries = array->data; +// const uint32_t h = zend_stricthash_hash_uint32_t(key); +// zend_strictmap_entry *entry = zend_strictmap_find_entry(array, key, h); +// if (!entry) { +// return false; +// } +// ZEND_ASSERT(SM_IT_HASH(entry) == h); +// +// const uint32_t nIndex = h | array->hash_mask; +// uint32_t i = SM_HASH(array, nIndex); +// +// const uint32_t idx = entry - entries; +// if (i == idx) { +// SM_HASH(array, nIndex) = SM_IT_NEXT(entry); +// } else { +// zend_strictmap_entry *prev = &array->data[i]; +// while (SM_IT_NEXT(prev) != idx) { +// i = SM_IT_NEXT(prev); +// prev = &array->data[i]; +// } +// SM_IT_NEXT(prev) = SM_IT_NEXT(entry); +// } +// +// array->count--; +// +// SM_IT_NEXT(entry) = array->free_index_chain; +// array->free_index_chain = idx; +// +// if (array->first_used_index == idx) { +// if (array->count == 0) { +// ZEND_ASSERT(array->used_indices == 0); +// array->first_used_index = 0; +// } else { +// ZEND_ASSERT(array->first_used_index < array->used_indices); +// do { +// array->first_used_index++; +// } while (array->first_used_index < array->used_indices && (UNEXPECTED(Z_ISUNDEF(array->data[array->first_used_index].key)))); +// ZEND_ASSERT(array->first_used_index < array->used_indices); +// } +// } +// zval old_key; +// zval old_value; +// ZVAL_COPY_VALUE(&old_key, &entry->key); +// ZVAL_COPY_VALUE(&old_value, &entry->value); +// ZEND_ASSERT(array->count <= array->used_indices); +// ZEND_ASSERT(array->first_used_index < array->used_indices || (array->first_used_index == array->used_indices && array->count == 0)); +// ZVAL_UNDEF(&entry->key); +// ZVAL_UNDEF(&entry->value); +// +// return true; +// } + +static size_t zend_strictmap_offset_bytes_for_capacity(uint32_t capacity) { + return capacity * sizeof(uint32_t); +} + +static zend_strictmap_entry *zend_strictmap_alloc_entries(uint32_t capacity) { + uint8_t *ptr = safe_emalloc(capacity, SM_MEMORY_PER_ENTRY, 0); + const size_t buckets_byte_count = zend_strictmap_offset_bytes_for_capacity(capacity); + memset(ptr, SM_INVALID_INDEX, buckets_byte_count); + return (void *)(ptr + buckets_byte_count); +} + +static void zend_strictmap_free_entries(zend_strictmap_entry *old_entries, uint32_t old_capacity) { + void * old_ptr = ((uint8_t *) old_entries) - zend_strictmap_offset_bytes_for_capacity(old_capacity); + efree(old_ptr); +} + +static void zend_strictmap_grow(zend_strictmap *array) +{ + ZEND_ASSERT(array->used_indices >= array->count); + if (UNEXPECTED(array->capacity >= HT_MAX_SIZE)) { + zend_error_noreturn(E_ERROR, "exceeded max valid strictmap capacity"); + } + + const uint32_t new_capacity = array->capacity * 2; + zend_strictmap_entry *const orig_entries = array->data; + zend_strictmap_entry *const new_entries = zend_strictmap_alloc_entries(new_capacity); + zend_strictmap_entry *old_entry; + zend_strictmap_entry *it = new_entries; + ZEND_ASSERT(array->count <= array->used_indices); + uint32_t i = 0; + const uint32_t new_mask = SM_SIZE_TO_MASK(new_capacity); + ZEND_STRICTMAP_FOREACH_BUCKET(array, old_entry) { + const uint32_t h = SM_IT_HASH(old_entry); + const uint32_t nIndex = h | new_mask; + ZVAL_COPY_VALUE(&it->key, &old_entry->key); + SM_IT_NEXT(it) = SM_HASH_EX(new_entries, nIndex); + ZVAL_COPY_VALUE(&it->value, &old_entry->value); + SM_IT_HASH(it) = h; + SM_HASH_EX(new_entries, nIndex) = i; + it++; + i++; + } ZEND_STRICTMAP_FOREACH_END(); + ZEND_ASSERT((size_t)(it - new_entries) == array->count); + zend_strictmap_free_entries(orig_entries, array->capacity); + + array->data = new_entries; + array->used_indices = array->count; + array->capacity = new_capacity; + array->hash_mask = new_mask; + array->first_used_index = 0; + array->free_index_chain = SM_INVALID_INDEX; +} + +static zend_strictmap_entry *zend_strictmap_find_entry(const zend_strictmap *ht, zval *key, const uint32_t h) +{ + zend_strictmap_entry *p; + + zend_strictmap_entry *const data = ht->data; + const uint32_t nIndex = h | ht->hash_mask; + uint32_t idx = SM_HASH_EX(data, nIndex); + while (idx != SM_INVALID_INDEX) { + ZEND_ASSERT(idx < ht->capacity); + p = data + idx; + if (SM_IT_HASH(p) == h && zend_is_identical(&p->key, key)) { + return p; + } + idx = SM_IT_NEXT(p); + } + return NULL; +} diff --git a/Zend/zend_strictmap.h b/Zend/zend_strictmap.h new file mode 100644 index 0000000000000..3e2fa37c09156 --- /dev/null +++ b/Zend/zend_strictmap.h @@ -0,0 +1,84 @@ +/* + Copyright (c) 2021 Tyson Andre + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of the 'teds' nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ZEND_STRICTMAP_H +#define ZEND_STRICTMAP_H + +#include "Zend/zend_types.h" + +#define ZEND_STRICTMAP_FOREACH(_stricthashmap) do { \ + const zend_strictmap *const __stricthashmap = (_stricthashmap); \ + zend_strictmap_entry *_p = __stricthashmap->data + __stricthashmap->first_used_index; \ + zend_strictmap_entry *const _end = __stricthashmap->data + __stricthashmap->used_indices; \ + ZEND_ASSERT(__stricthashmap->first_used_index <= __stricthashmap->used_indices); \ + for (; _p != _end; _p++) { \ + zval *_key = &_p->key; \ + if (Z_TYPE_P(_key) == IS_UNDEF) { continue; } + +#define ZEND_STRICTMAP_FOREACH_KEY_VAL(stricthashmap, k, v) ZEND_STRICTMAP_FOREACH(stricthashmap) \ + k = _key; \ + v = &_p->value; + +#define ZEND_STRICTMAP_FOREACH_KEY(stricthashmap, k) ZEND_STRICTMAP_FOREACH(stricthashmap) \ + k = _key; + +#define ZEND_STRICTMAP_FOREACH_BUCKET(stricthashmap, b) ZEND_STRICTMAP_FOREACH(stricthashmap) \ + b = _p; + +#define ZEND_STRICTMAP_FOREACH_END() \ + } \ +} while (0) + +typedef struct _zend_strictmap_entry { + zval key; + zval value; +} zend_strictmap_entry; + +/* See Zend/zend_types.h for the zend_array layout this is based on. */ +typedef struct _zend_strictmap { + zend_strictmap_entry *data; + uint32_t count; /* Number of elements. */ + uint32_t capacity; /* Power of 2 size. */ + uint32_t used_indices; /* Number of buckets used, including gaps left by remove. */ + uint32_t hash_mask; /* -capacity or ZEND_STRICTMAP_MIN_MASK, e.g. 0xfffffff0 for an array of size 8 with 16 buckets. */ + uint32_t first_used_index; /* The offset of the first bucket used. */ + uint32_t free_index_chain; /* Chain of freed indicies that can be reused. */ +} zend_strictmap; + +void zend_strictmap_dtor(zend_strictmap *array); +void zend_strictmap_init(zend_strictmap *array); +bool zend_strictmap_insert(zend_strictmap *array, zval *key, zval *value); +zend_strictmap_entry *zend_strictmap_find_value(const zend_strictmap *array, zval *value); +// Unused and untested +//bool zend_strictmap_remove_key(zend_strictmap *array, zval *key); + +#endif /* ZEND_STRICTMAP_H */ diff --git a/bench.php b/bench.php new file mode 100644 index 0000000000000..ec3a46954999c --- /dev/null +++ b/bench.php @@ -0,0 +1,35 @@ + $a]; +$b3 = (object)['foo' => $b]; +var_dump(array_unique([$a, $b, $a2, $b2, $a3, $b3], ARRAY_UNIQUE_IDENTICAL)); + +?> +--EXPECT-- +array(1) { + [0]=> + string(4) "1234" +} +array(3) { + [0]=> + string(4) "1234" + [1]=> + int(1234) + [2]=> + float(1234) +} +array(6) { + [0]=> + int(0) + [1]=> + string(1) "0" + [2]=> + float(0) + [3]=> + string(3) "0.0" + [4]=> + string(0) "" + [5]=> + NULL +} +array(6) { + [0]=> + object(stdClass)#1 (0) { + } + [1]=> + object(stdClass)#2 (0) { + } + [2]=> + array(1) { + [0]=> + object(stdClass)#1 (0) { + } + } + [3]=> + array(1) { + [0]=> + object(stdClass)#2 (0) { + } + } + [4]=> + object(stdClass)#3 (1) { + ["foo"]=> + object(stdClass)#1 (0) { + } + } + [5]=> + object(stdClass)#4 (1) { + ["foo"]=> + object(stdClass)#2 (0) { + } + } +} diff --git a/ext/standard/tests/array/array_unique_identical_enums.phpt b/ext/standard/tests/array/array_unique_identical_enums.phpt new file mode 100644 index 0000000000000..ab535b644394f --- /dev/null +++ b/ext/standard/tests/array/array_unique_identical_enums.phpt @@ -0,0 +1,83 @@ +--TEST-- +array_unique() with ARRAY_UNIQUE_IDENTICAL and enums +--FILE-- + Foo::Bar, + 'b' => Foo::Baz, + 'c' => Foo::Bar, +], ARRAY_UNIQUE_IDENTICAL)); + +var_dump(array_unique([ + &$bar, + Foo::Bar, + &$bar2, + Foo::Baz, +], ARRAY_UNIQUE_IDENTICAL)); + +$value2 = "hello"; +$value3 = 0; +$value4 = &$value2; +var_dump(array_unique([ + 0, + &$value4, + &$value2, + "hello", + &$value3, + $value4 +], ARRAY_UNIQUE_IDENTICAL)); + +?> +--EXPECT-- +array(2) { + [0]=> + enum(Foo::Bar) + [1]=> + enum(Foo::Baz) +} +array(2) { + [0]=> + enum(Foo::Bar) + [2]=> + enum(Foo::Baz) +} +array(2) { + ["a"]=> + enum(Foo::Bar) + ["b"]=> + enum(Foo::Baz) +} +array(2) { + [0]=> + &enum(Foo::Bar) + [3]=> + enum(Foo::Baz) +} +array(2) { + [0]=> + int(0) + [1]=> + &string(5) "hello" +} diff --git a/ext/standard/tests/array/array_unique_identical_nan.phpt b/ext/standard/tests/array/array_unique_identical_nan.phpt new file mode 100644 index 0000000000000..a97bb919856c5 --- /dev/null +++ b/ext/standard/tests/array/array_unique_identical_nan.phpt @@ -0,0 +1,77 @@ +--TEST-- +array_unique() with ARRAY_UNIQUE_IDENTICAL preserves NAN +--FILE-- + NAN, + 'bar' => NAN, +], ARRAY_UNIQUE_IDENTICAL)); + +var_dump(array_unique([ + [NAN], + 1.0, + [NAN], +], ARRAY_UNIQUE_IDENTICAL)); + +var_dump(array_unique([ + 1.0, + NAN, + 1.0, + NAN, + 1.0, +], ARRAY_UNIQUE_IDENTICAL)); + +var_dump(array_unique([ + 1.0, + NAN, + 1.0, +], ARRAY_UNIQUE_IDENTICAL)); + +?> +--EXPECT-- +array(2) { + [0]=> + float(NAN) + [1]=> + float(NAN) +} +array(2) { + ["foo"]=> + float(NAN) + ["bar"]=> + float(NAN) +} +array(3) { + [0]=> + array(1) { + [0]=> + float(NAN) + } + [1]=> + float(1) + [2]=> + array(1) { + [0]=> + float(NAN) + } +} +array(3) { + [0]=> + float(1) + [1]=> + float(NAN) + [3]=> + float(NAN) +} +array(2) { + [0]=> + float(1) + [1]=> + float(NAN) +} diff --git a/ext/standard/tests/array/array_unique_identical_recursion.phpt b/ext/standard/tests/array/array_unique_identical_recursion.phpt new file mode 100644 index 0000000000000..7507b759b0f62 --- /dev/null +++ b/ext/standard/tests/array/array_unique_identical_recursion.phpt @@ -0,0 +1,13 @@ +--TEST-- +Test array_unique() with ARRAY_UNIQUE_IDENTICAL and recursion +--FILE-- + +--EXPECTF-- +Fatal error: Nesting level too deep - recursive dependency? in %s on line %d diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 0b1736c8ab70d..65967e3ffc0e2 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -240,7 +240,7 @@ ADD_SOURCES("Zend", "zend_language_parser.c zend_language_scanner.c \ zend_default_classes.c zend_execute.c zend_strtod.c zend_gc.c zend_closures.c zend_weakrefs.c \ zend_float.c zend_string.c zend_generators.c zend_virtual_cwd.c zend_ast.c \ zend_inheritance.c zend_smart_str.c zend_cpuinfo.c zend_observer.c zend_system_id.c \ - zend_enum.c zend_fibers.c zend_atomic.c"); + zend_enum.c zend_fibers.c zend_atomic.c zend_strictmap.c zend_stricthash.c"); ADD_SOURCES("Zend\\Optimizer", "zend_optimizer.c pass1.c pass3.c optimize_func_calls.c block_pass.c optimize_temp_vars_5.c nop_removal.c compact_literals.c zend_cfg.c zend_dfg.c dfa_pass.c zend_ssa.c zend_inference.c zend_func_info.c zend_call_graph.c zend_dump.c escape_analysis.c compact_vars.c dce.c sccp.c scdf.c"); var PHP_ASSEMBLER = PATH_PROG({ From 336aa6710914b5d5f3fb97aa0007e623ffd9b7a6 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 1 Dec 2022 13:55:07 +0100 Subject: [PATCH 2/2] Switch to namespaced Assoc/List \unique functions --- bench.php | 6 +- ext/standard/array.c | 99 ++++++++++++------- ext/standard/basic_functions.stub.php | 23 ++++- ext/standard/basic_functions_arginfo.h | 11 ++- ext/standard/php_array.h | 4 - ...nique_identical.phpt => assoc_unique.phpt} | 10 +- ...cal_enums.phpt => assoc_unique_enums.phpt} | 22 ++--- ...entical_nan.phpt => assoc_unique_nan.phpt} | 22 ++--- ...rsion.phpt => assoc_unique_recursion.phpt} | 4 +- ext/standard/tests/array/list_unique.phpt | 77 +++++++++++++++ .../tests/array/list_unique_enums.phpt | 83 ++++++++++++++++ ext/standard/tests/array/list_unique_nan.phpt | 77 +++++++++++++++ .../tests/array/list_unique_recursion.phpt | 13 +++ 13 files changed, 371 insertions(+), 80 deletions(-) rename ext/standard/tests/array/{array_unique_identical.phpt => assoc_unique.phpt} (68%) rename ext/standard/tests/array/{array_unique_identical_enums.phpt => assoc_unique_enums.phpt} (70%) rename ext/standard/tests/array/{array_unique_identical_nan.phpt => assoc_unique_nan.phpt} (64%) rename ext/standard/tests/array/{array_unique_identical_recursion.phpt => assoc_unique_recursion.phpt} (58%) create mode 100644 ext/standard/tests/array/list_unique.phpt create mode 100644 ext/standard/tests/array/list_unique_enums.phpt create mode 100644 ext/standard/tests/array/list_unique_nan.phpt create mode 100644 ext/standard/tests/array/list_unique_recursion.phpt diff --git a/bench.php b/bench.php index ec3a46954999c..aba2d4872d016 100644 --- a/bench.php +++ b/bench.php @@ -9,12 +9,12 @@ function generate_values(int $n) { return $values; } -function bench_array_unique_identical(int $n, int $iterations) { +function bench_assoc_unique_identical(int $n, int $iterations) { $values = generate_values($n); $start = hrtime(true); $sum = 0; for ($i = 0; $i < $iterations; $i++) { - $sum += array_sum(array_unique($values, ARRAY_UNIQUE_IDENTICAL)); + $sum += array_sum(Assoc\unique($values)); } $end = hrtime(true); printf("%30s n=%8d iterations=%8d time=%.3f sum=%d\n", __FUNCTION__, $n, $iterations, ($end - $start)/1e9, $sum); @@ -31,5 +31,5 @@ function bench_array_unique_identical(int $n, int $iterations) { ]; foreach ($sizes as [$n, $iterations]) { - bench_array_unique_identical($n, $iterations); + bench_assoc_unique_identical($n, $iterations); } diff --git a/ext/standard/array.c b/ext/standard/array.c index af20a6601cf6f..4b082d9d280c0 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -42,7 +42,7 @@ #include "zend_exceptions.h" #include "ext/spl/spl_array.h" #include "ext/random/php_random.h" -#include "Zend/zend_strictmap.h" +#include "zend_strictmap.h" /* {{{ defines */ @@ -4560,42 +4560,6 @@ PHP_FUNCTION(array_unique) return; } - if (sort_type == PHP_ARRAY_UNIQUE_IDENTICAL) { - zend_long num_key; - zend_string *str_key; - zval *val; - zend_strictmap seen; - - zend_strictmap_init(&seen); - - array_init(return_value); - - ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_key, str_key, val) { - zval dummy; - ZVAL_TRUE(&dummy); - zval *val_deref = val; - ZVAL_DEREF(val_deref); - bool added = zend_strictmap_insert(&seen, val_deref, &dummy); - - if (added) { - /* First occurrence of the value */ - if (UNEXPECTED(Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1)) { - ZVAL_DEREF(val); - } - Z_TRY_ADDREF_P(val); - - if (str_key) { - zend_hash_add_new(Z_ARRVAL_P(return_value), str_key, val); - } else { - zend_hash_index_add_new(Z_ARRVAL_P(return_value), num_key, val); - } - } - } ZEND_HASH_FOREACH_END(); - - zend_strictmap_dtor(&seen); - return; - } - cmp = php_get_data_compare_func_unstable(sort_type, 0); RETVAL_ARR(zend_array_dup(Z_ARRVAL_P(array))); @@ -6492,3 +6456,64 @@ PHP_FUNCTION(array_combine) } ZEND_HASH_FOREACH_END(); } /* }}} */ + +static zend_always_inline void list_assoc_unique(INTERNAL_FUNCTION_PARAMETERS, bool preserve_keys) +{ + zval *array; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(array) + ZEND_PARSE_PARAMETERS_END(); + + if (Z_ARRVAL_P(array)->nNumOfElements <= 1) { + ZVAL_COPY(return_value, array); + return; + } + + zend_long num_key; + zend_string *str_key; + zval *val; + zend_strictmap seen; + + zend_strictmap_init(&seen); + + array_init(return_value); + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_key, str_key, val) { + zval dummy; + ZVAL_TRUE(&dummy); + zval *val_deref = val; + ZVAL_DEREF(val_deref); + bool added = zend_strictmap_insert(&seen, val_deref, &dummy); + + if (added) { + /* First occurrence of the value */ + if (UNEXPECTED(Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1)) { + ZVAL_DEREF(val); + } + Z_TRY_ADDREF_P(val); + + if (preserve_keys) { + if (str_key) { + zend_hash_add_new(Z_ARRVAL_P(return_value), str_key, val); + } else { + zend_hash_index_add_new(Z_ARRVAL_P(return_value), num_key, val); + } + } else { + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), val); + } + } + } ZEND_HASH_FOREACH_END(); + + zend_strictmap_dtor(&seen); +} + +ZEND_FUNCTION(List_unique) +{ + list_assoc_unique(INTERNAL_FUNCTION_PARAM_PASSTHRU, /* preserve_keys */ false); +} + +ZEND_FUNCTION(Assoc_unique) +{ + list_assoc_unique(INTERNAL_FUNCTION_PARAM_PASSTHRU, /* preserve_keys */ true); +} diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 3089f1a928179..217440326a0ed 100755 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2,6 +2,8 @@ /** @generate-class-entries */ +namespace { + /* array.c */ /** @@ -87,11 +89,6 @@ */ const SORT_FLAG_CASE = UNKNOWN; -/** - * @var int - * @cvalue PHP_ARRAY_UNIQUE_IDENTICAL - */ -const ARRAY_UNIQUE_IDENTICAL = UNKNOWN; /** * @var int @@ -3536,3 +3533,19 @@ function sapi_windows_set_ctrl_handler(?callable $handler, bool $add = true): bo function sapi_windows_generate_ctrl_event(int $event, int $pid = 0): bool {} #endif + +} + +namespace List { + /** + * @compile-time-eval + */ + function unique(array $array): array {} +} + +namespace Assoc { + /** + * @compile-time-eval + */ + function unique(array $array): array {} +} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 95c54e68f0e5e..ace45ebd72f77 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 122cb89e08a3a4c2d8c0f69120490dbe527037a9 */ + * Stub hash: ae6ae27b7e077ca1f46e02d65b9c140569ee6c04 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -2202,6 +2202,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_sapi_windows_generate_ctrl_event ZEND_END_ARG_INFO() #endif +#define arginfo_List_unique arginfo_array_values + +#define arginfo_Assoc_unique arginfo_array_values + ZEND_FUNCTION(set_time_limit); ZEND_FUNCTION(header_register_callback); @@ -2820,6 +2824,8 @@ ZEND_FUNCTION(sapi_windows_set_ctrl_handler); #if defined(PHP_WIN32) ZEND_FUNCTION(sapi_windows_generate_ctrl_event); #endif +ZEND_FUNCTION(List_unique); +ZEND_FUNCTION(Assoc_unique); static const zend_function_entry ext_functions[] = { @@ -3468,6 +3474,8 @@ static const zend_function_entry ext_functions[] = { #if defined(PHP_WIN32) ZEND_FE(sapi_windows_generate_ctrl_event, arginfo_sapi_windows_generate_ctrl_event) #endif + ZEND_NS_FALIAS("List", unique, List_unique, arginfo_List_unique) + ZEND_NS_FALIAS("Assoc", unique, Assoc_unique, arginfo_Assoc_unique) ZEND_FE_END }; @@ -3499,7 +3507,6 @@ static void register_basic_functions_symbols(int module_number) REGISTER_LONG_CONSTANT("SORT_LOCALE_STRING", PHP_SORT_LOCALE_STRING, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("SORT_NATURAL", PHP_SORT_NATURAL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("SORT_FLAG_CASE", PHP_SORT_FLAG_CASE, CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("ARRAY_UNIQUE_IDENTICAL", PHP_ARRAY_UNIQUE_IDENTICAL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CASE_LOWER", PHP_CASE_LOWER, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CASE_UPPER", PHP_CASE_UPPER, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("COUNT_NORMAL", PHP_COUNT_NORMAL, CONST_PERSISTENT); diff --git a/ext/standard/php_array.h b/ext/standard/php_array.h index f389e300f876b..5d42a22f42040 100644 --- a/ext/standard/php_array.h +++ b/ext/standard/php_array.h @@ -54,12 +54,8 @@ PHPAPI bool php_array_pick_keys(const php_random_algo *algo, php_random_status * #define PHP_SORT_ASC 4 #define PHP_SORT_LOCALE_STRING 5 #define PHP_SORT_NATURAL 6 -//#define PHP_DONT_USE 7 #define PHP_SORT_FLAG_CASE 8 -// Must not clash with the PHP_SORT_ flags -#define PHP_ARRAY_UNIQUE_IDENTICAL 7 - #define PHP_COUNT_NORMAL 0 #define PHP_COUNT_RECURSIVE 1 diff --git a/ext/standard/tests/array/array_unique_identical.phpt b/ext/standard/tests/array/assoc_unique.phpt similarity index 68% rename from ext/standard/tests/array/array_unique_identical.phpt rename to ext/standard/tests/array/assoc_unique.phpt index b0bbf2ca72122..0cd42662d4624 100644 --- a/ext/standard/tests/array/array_unique_identical.phpt +++ b/ext/standard/tests/array/assoc_unique.phpt @@ -1,11 +1,11 @@ --TEST-- -Test array_unique() with ARRAY_UNIQUE_IDENTICAL +Test Assoc\unique() --FILE-- $a]; $b3 = (object)['foo' => $b]; -var_dump(array_unique([$a, $b, $a2, $b2, $a3, $b3], ARRAY_UNIQUE_IDENTICAL)); +var_dump(Assoc\unique([$a, $b, $a2, $b2, $a3, $b3])); ?> --EXPECT-- diff --git a/ext/standard/tests/array/array_unique_identical_enums.phpt b/ext/standard/tests/array/assoc_unique_enums.phpt similarity index 70% rename from ext/standard/tests/array/array_unique_identical_enums.phpt rename to ext/standard/tests/array/assoc_unique_enums.phpt index ab535b644394f..2d5d7ba5cef02 100644 --- a/ext/standard/tests/array/array_unique_identical_enums.phpt +++ b/ext/standard/tests/array/assoc_unique_enums.phpt @@ -1,5 +1,5 @@ --TEST-- -array_unique() with ARRAY_UNIQUE_IDENTICAL and enums +Assoc\unique() and enums --FILE-- Foo::Bar, 'b' => Foo::Baz, 'c' => Foo::Bar, -], ARRAY_UNIQUE_IDENTICAL)); +])); -var_dump(array_unique([ +var_dump(Assoc\unique([ &$bar, Foo::Bar, &$bar2, Foo::Baz, -], ARRAY_UNIQUE_IDENTICAL)); +])); $value2 = "hello"; $value3 = 0; $value4 = &$value2; -var_dump(array_unique([ +var_dump(Assoc\unique([ 0, &$value4, &$value2, "hello", &$value3, $value4 -], ARRAY_UNIQUE_IDENTICAL)); +])); ?> --EXPECT-- diff --git a/ext/standard/tests/array/array_unique_identical_nan.phpt b/ext/standard/tests/array/assoc_unique_nan.phpt similarity index 64% rename from ext/standard/tests/array/array_unique_identical_nan.phpt rename to ext/standard/tests/array/assoc_unique_nan.phpt index a97bb919856c5..06c631f5cb229 100644 --- a/ext/standard/tests/array/array_unique_identical_nan.phpt +++ b/ext/standard/tests/array/assoc_unique_nan.phpt @@ -1,37 +1,37 @@ --TEST-- -array_unique() with ARRAY_UNIQUE_IDENTICAL preserves NAN +Assoc\unique() preserves NAN --FILE-- NAN, 'bar' => NAN, -], ARRAY_UNIQUE_IDENTICAL)); +])); -var_dump(array_unique([ +var_dump(Assoc\unique([ [NAN], 1.0, [NAN], -], ARRAY_UNIQUE_IDENTICAL)); +])); -var_dump(array_unique([ +var_dump(Assoc\unique([ 1.0, NAN, 1.0, NAN, 1.0, -], ARRAY_UNIQUE_IDENTICAL)); +])); -var_dump(array_unique([ +var_dump(Assoc\unique([ 1.0, NAN, 1.0, -], ARRAY_UNIQUE_IDENTICAL)); +])); ?> --EXPECT-- diff --git a/ext/standard/tests/array/array_unique_identical_recursion.phpt b/ext/standard/tests/array/assoc_unique_recursion.phpt similarity index 58% rename from ext/standard/tests/array/array_unique_identical_recursion.phpt rename to ext/standard/tests/array/assoc_unique_recursion.phpt index 7507b759b0f62..0c23680b56015 100644 --- a/ext/standard/tests/array/array_unique_identical_recursion.phpt +++ b/ext/standard/tests/array/assoc_unique_recursion.phpt @@ -1,12 +1,12 @@ --TEST-- -Test array_unique() with ARRAY_UNIQUE_IDENTICAL and recursion +Test Assoc\unique() and recursion --FILE-- --EXPECTF-- diff --git a/ext/standard/tests/array/list_unique.phpt b/ext/standard/tests/array/list_unique.phpt new file mode 100644 index 0000000000000..cefd3b4b0167f --- /dev/null +++ b/ext/standard/tests/array/list_unique.phpt @@ -0,0 +1,77 @@ +--TEST-- +Test List\unique() +--FILE-- + $a]; +$b3 = (object)['foo' => $b]; +var_dump(List\unique([$a, $b, $a2, $b2, $a3, $b3])); + +?> +--EXPECT-- +array(1) { + [0]=> + string(4) "1234" +} +array(3) { + [0]=> + string(4) "1234" + [1]=> + int(1234) + [2]=> + float(1234) +} +array(6) { + [0]=> + int(0) + [1]=> + string(1) "0" + [2]=> + float(0) + [3]=> + string(3) "0.0" + [4]=> + string(0) "" + [5]=> + NULL +} +array(6) { + [0]=> + object(stdClass)#1 (0) { + } + [1]=> + object(stdClass)#2 (0) { + } + [2]=> + array(1) { + [0]=> + object(stdClass)#1 (0) { + } + } + [3]=> + array(1) { + [0]=> + object(stdClass)#2 (0) { + } + } + [4]=> + object(stdClass)#3 (1) { + ["foo"]=> + object(stdClass)#1 (0) { + } + } + [5]=> + object(stdClass)#4 (1) { + ["foo"]=> + object(stdClass)#2 (0) { + } + } +} diff --git a/ext/standard/tests/array/list_unique_enums.phpt b/ext/standard/tests/array/list_unique_enums.phpt new file mode 100644 index 0000000000000..6f025b0a295bf --- /dev/null +++ b/ext/standard/tests/array/list_unique_enums.phpt @@ -0,0 +1,83 @@ +--TEST-- +List\unique() and enums +--FILE-- + Foo::Bar, + 'b' => Foo::Baz, + 'c' => Foo::Bar, +])); + +var_dump(List\unique([ + &$bar, + Foo::Bar, + &$bar2, + Foo::Baz, +])); + +$value2 = "hello"; +$value3 = 0; +$value4 = &$value2; +var_dump(List\unique([ + 0, + &$value4, + &$value2, + "hello", + &$value3, + $value4 +])); + +?> +--EXPECT-- +array(2) { + [0]=> + enum(Foo::Bar) + [1]=> + enum(Foo::Baz) +} +array(2) { + [0]=> + enum(Foo::Bar) + [1]=> + enum(Foo::Baz) +} +array(2) { + [0]=> + enum(Foo::Bar) + [1]=> + enum(Foo::Baz) +} +array(2) { + [0]=> + &enum(Foo::Bar) + [1]=> + enum(Foo::Baz) +} +array(2) { + [0]=> + int(0) + [1]=> + &string(5) "hello" +} diff --git a/ext/standard/tests/array/list_unique_nan.phpt b/ext/standard/tests/array/list_unique_nan.phpt new file mode 100644 index 0000000000000..4246690d7a415 --- /dev/null +++ b/ext/standard/tests/array/list_unique_nan.phpt @@ -0,0 +1,77 @@ +--TEST-- +List\unique() preserves NAN +--FILE-- + NAN, + 'bar' => NAN, +])); + +var_dump(List\unique([ + [NAN], + 1.0, + [NAN], +])); + +var_dump(List\unique([ + 1.0, + NAN, + 1.0, + NAN, + 1.0, +])); + +var_dump(List\unique([ + 1.0, + NAN, + 1.0, +])); + +?> +--EXPECT-- +array(2) { + [0]=> + float(NAN) + [1]=> + float(NAN) +} +array(2) { + [0]=> + float(NAN) + [1]=> + float(NAN) +} +array(3) { + [0]=> + array(1) { + [0]=> + float(NAN) + } + [1]=> + float(1) + [2]=> + array(1) { + [0]=> + float(NAN) + } +} +array(3) { + [0]=> + float(1) + [1]=> + float(NAN) + [2]=> + float(NAN) +} +array(2) { + [0]=> + float(1) + [1]=> + float(NAN) +} diff --git a/ext/standard/tests/array/list_unique_recursion.phpt b/ext/standard/tests/array/list_unique_recursion.phpt new file mode 100644 index 0000000000000..8dd9869e71178 --- /dev/null +++ b/ext/standard/tests/array/list_unique_recursion.phpt @@ -0,0 +1,13 @@ +--TEST-- +Test List\unique() and recursion +--FILE-- + +--EXPECTF-- +Fatal error: Nesting level too deep - recursive dependency? in %s on line %d