From 27cd7a11cb9f0dc9f2a906a659775e221eb87efa Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Thu, 7 Jan 2021 10:49:50 +0100 Subject: Add support for string keys in array unpacking This adds support for: $array1 = ['a' => 1, 'b' => 2]; $array2 = ['b' => 3, 'c' => 4]; $array = [...$array1, ...$array2]; // => ['a' => 1, 'b' => 3, 'c' => 4] RFC: https://wiki.php.net/rfc/array_unpacking_string_keys Closes GH-6584. --- UPGRADING | 5 +- Zend/Optimizer/sccp.c | 5 +- Zend/Optimizer/zend_inference.c | 7 +-- Zend/tests/array_unpack/non_integer_keys.phpt | 4 +- Zend/tests/array_unpack/string_keys.phpt | 64 +++++++++++++++++----- .../unpack_string_keys_compile_time.phpt | 19 ++++++- Zend/zend_ast.c | 5 +- Zend/zend_compile.c | 5 +- Zend/zend_vm_def.h | 42 ++++++++------ Zend/zend_vm_execute.h | 42 ++++++++------ 10 files changed, 133 insertions(+), 65 deletions(-) diff --git a/UPGRADING b/UPGRADING index 29701c3714..3014597868 100644 --- a/UPGRADING +++ b/UPGRADING @@ -82,8 +82,11 @@ PHP 8.1 UPGRADE NOTES - Core: . It is now possible to specify octal integer by using the explicit "0o"/"0O" - prefix similar to hexadecimal ("0x"/"0X) and binary ("0b"/"0B") integer literals + prefix similar to hexadecimal ("0x"/"0X) and binary ("0b"/"0B") integer + literals. RFC: https://wiki.php.net/rfc/explicit_octal_notation + . Added support for array unpacking with strings keys. + RFC: https://wiki.php.net/rfc/array_unpacking_string_keys - Curl: . Added CURLOPT_DOH_URL option. diff --git a/Zend/Optimizer/sccp.c b/Zend/Optimizer/sccp.c index e097f654c8..490fbdf16b 100644 --- a/Zend/Optimizer/sccp.c +++ b/Zend/Optimizer/sccp.c @@ -575,9 +575,10 @@ static inline int ct_eval_add_array_unpack(zval *result, zval *array) { SEPARATE_ARRAY(result); ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(array), key, value) { if (key) { - return FAILURE; + value = zend_hash_update(Z_ARR_P(result), key, value); + } else { + value = zend_hash_next_index_insert(Z_ARR_P(result), value); } - value = zend_hash_next_index_insert(Z_ARR_P(result), value); if (!value) { return FAILURE; } diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index ba51283beb..a61a18176c 100644 --- a/Zend/Optimizer/zend_inference.c +++ b/Zend/Optimizer/zend_inference.c @@ -3163,12 +3163,9 @@ static zend_always_inline int _zend_update_type_info( case ZEND_ADD_ARRAY_UNPACK: tmp = ssa_var_info[ssa_op->result_use].type; ZEND_ASSERT(tmp & MAY_BE_ARRAY); - /* Ignore string keys as they will throw. */ - if (t1 & MAY_BE_ARRAY_KEY_LONG) { - tmp |= MAY_BE_ARRAY_KEY_LONG | (t1 & (MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF)); - } + tmp |= t1 & (MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF); if (t1 & MAY_BE_OBJECT) { - tmp |= MAY_BE_ARRAY_KEY_LONG | MAY_BE_ARRAY_OF_ANY; + tmp |= MAY_BE_ARRAY_KEY_ANY | MAY_BE_ARRAY_OF_ANY; } UPDATE_SSA_TYPE(tmp, ssa_op->result_def); break; diff --git a/Zend/tests/array_unpack/non_integer_keys.phpt b/Zend/tests/array_unpack/non_integer_keys.phpt index a5e407743c..ab7a20ac86 100644 --- a/Zend/tests/array_unpack/non_integer_keys.phpt +++ b/Zend/tests/array_unpack/non_integer_keys.phpt @@ -1,5 +1,5 @@ --TEST-- -Array unpacking does not work with non-integer keys +Array unpacking does not work with non-integer/string keys --FILE-- --EXPECT-- -Exception: Cannot unpack Traversable with non-integer keys +Exception: Keys must be of type int|string during array unpacking diff --git a/Zend/tests/array_unpack/string_keys.phpt b/Zend/tests/array_unpack/string_keys.phpt index e4cfd77f58..d446e69cab 100644 --- a/Zend/tests/array_unpack/string_keys.phpt +++ b/Zend/tests/array_unpack/string_keys.phpt @@ -1,22 +1,58 @@ --TEST-- -array unpacking with string keys (not supported) +Array unpacking with string keys --FILE-- 3, 4]; - var_dump([...$array]); -} catch (Error $ex) { - var_dump($ex->getMessage()); -} -try { - $iterator = new ArrayIterator([1, 2, "foo" => 3, 4]); - var_dump([...$iterator]); -} catch (Error $ex) { - var_dump($ex->getMessage()); +// Works with both arrays and Traversables. +$array = [1, 2, "foo" => 3, 4]; +var_dump([...$array]); + +$iterator = new ArrayIterator([1, 2, "foo" => 3, 4]); +var_dump([...$iterator]); + +// Test overwriting behavior. +$array1 = ["foo" => 1]; +$array2 = ["foo" => 2]; +var_dump(["foo" => 0, ...$array1, ...$array2]); +var_dump(["foo" => 0, ...$array1, ...$array2, "foo" => 3]); + +// Test numeric string key from iterator. +function gen() { + yield "42" => 42; } +var_dump([...gen()]); ?> --EXPECT-- -string(36) "Cannot unpack array with string keys" -string(42) "Cannot unpack Traversable with string keys" +array(4) { + [0]=> + int(1) + [1]=> + int(2) + ["foo"]=> + int(3) + [2]=> + int(4) +} +array(4) { + [0]=> + int(1) + [1]=> + int(2) + ["foo"]=> + int(3) + [2]=> + int(4) +} +array(1) { + ["foo"]=> + int(2) +} +array(1) { + ["foo"]=> + int(3) +} +array(1) { + [0]=> + int(42) +} diff --git a/Zend/tests/array_unpack/unpack_string_keys_compile_time.phpt b/Zend/tests/array_unpack/unpack_string_keys_compile_time.phpt index 1401fb9bd5..df58d78a6a 100644 --- a/Zend/tests/array_unpack/unpack_string_keys_compile_time.phpt +++ b/Zend/tests/array_unpack/unpack_string_keys_compile_time.phpt @@ -1,10 +1,23 @@ --TEST-- -Unpacking of string keys detected at compile-time +Unpacking of string keys is supported at compile-time --FILE-- 'b']]); +var_dump(['a' => 'X', ...['a' => 'b']]); +var_dump([...['a' => 'b'], 'a' => 'X']); ?> ---EXPECTF-- -Fatal error: Cannot unpack array with string keys in %s on line %d +--EXPECT-- +array(1) { + ["a"]=> + string(1) "b" +} +array(1) { + ["a"]=> + string(1) "b" +} +array(1) { + ["a"]=> + string(1) "X" +} diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index b2af322d91..73f2390c11 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -485,16 +485,15 @@ static zend_result zend_ast_add_unpacked_element(zval *result, zval *expr) { ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) { if (key) { - zend_throw_error(NULL, "Cannot unpack array with string keys"); - return FAILURE; + zend_hash_update(Z_ARRVAL_P(result), key, val); } else { if (!zend_hash_next_index_insert(Z_ARRVAL_P(result), val)) { zend_throw_error(NULL, "Cannot add element to the array as the next element is already occupied"); return FAILURE; } - Z_TRY_ADDREF_P(val); } + Z_TRY_ADDREF_P(val); } ZEND_HASH_FOREACH_END(); return SUCCESS; } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index c671628479..65fa186234 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8100,9 +8100,8 @@ static bool zend_try_ct_eval_array(zval *result, zend_ast *ast) /* {{{ */ ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) { if (key) { - zend_error_noreturn(E_COMPILE_ERROR, "Cannot unpack array with string keys"); - } - if (!zend_hash_next_index_insert(Z_ARRVAL_P(result), val)) { + zend_hash_update(Z_ARRVAL_P(result), key, val); + } else if (!zend_hash_next_index_insert(Z_ARRVAL_P(result), val)) { zval_ptr_dtor(result); return 0; } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 2663945a66..4b927461a2 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -5952,9 +5952,11 @@ ZEND_VM_HANDLER(147, ZEND_ADD_ARRAY_UNPACK, ANY, ANY) { USE_OPLINE zval *op1; + HashTable *result_ht; SAVE_OPLINE(); op1 = GET_OP1_ZVAL_PTR(BP_VAR_R); + result_ht = Z_ARRVAL_P(EX_VAR(opline->result.var)); ZEND_VM_C_LABEL(add_unpack_again): if (EXPECTED(Z_TYPE_P(op1) == IS_ARRAY)) { @@ -5963,16 +5965,14 @@ ZEND_VM_C_LABEL(add_unpack_again): zend_string *key; ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) { + if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) { + val = Z_REFVAL_P(val); + } + Z_TRY_ADDREF_P(val); if (key) { - zend_throw_error(NULL, "Cannot unpack array with string keys"); - FREE_OP1(); - HANDLE_EXCEPTION(); + zend_hash_update(result_ht, key, val); } else { - if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) { - val = Z_REFVAL_P(val); - } - Z_TRY_ADDREF_P(val); - if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) { + if (!zend_hash_next_index_insert(result_ht, val)) { zend_cannot_add_element(); zval_ptr_dtor_nogc(val); break; @@ -6013,32 +6013,42 @@ ZEND_VM_C_LABEL(add_unpack_again): break; } + zval key; if (iter->funcs->get_current_key) { - zval key; iter->funcs->get_current_key(iter, &key); if (UNEXPECTED(EG(exception) != NULL)) { break; } - if (UNEXPECTED(Z_TYPE(key) != IS_LONG)) { + if (UNEXPECTED(Z_TYPE(key) != IS_LONG && Z_TYPE(key) != IS_STRING)) { zend_throw_error(NULL, - (Z_TYPE(key) == IS_STRING) ? - "Cannot unpack Traversable with string keys" : - "Cannot unpack Traversable with non-integer keys"); + "Keys must be of type int|string during array unpacking"); zval_ptr_dtor(&key); break; } + } else { + ZVAL_UNDEF(&key); } ZVAL_DEREF(val); Z_TRY_ADDREF_P(val); - if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) { - zend_cannot_add_element(); - zval_ptr_dtor_nogc(val); + zend_ulong num_key; + if (Z_TYPE(key) == IS_STRING && !ZEND_HANDLE_NUMERIC(Z_STR(key), num_key)) { + zend_hash_update(result_ht, Z_STR(key), val); + zval_ptr_dtor_str(&key); + } else { + if (!zend_hash_next_index_insert(result_ht, val)) { + zend_cannot_add_element(); + zval_ptr_dtor_nogc(val); + break; + } } iter->funcs->move_forward(iter); + if (UNEXPECTED(EG(exception))) { + break; + } } zend_iterator_dtor(iter); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index e4b1c03aee..2f1f6fc298 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -2499,9 +2499,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_ARRAY_UNPACK_SPEC_HANDLER( { USE_OPLINE zval *op1; + HashTable *result_ht; SAVE_OPLINE(); op1 = get_zval_ptr(opline->op1_type, opline->op1, BP_VAR_R); + result_ht = Z_ARRVAL_P(EX_VAR(opline->result.var)); add_unpack_again: if (EXPECTED(Z_TYPE_P(op1) == IS_ARRAY)) { @@ -2510,16 +2512,14 @@ add_unpack_again: zend_string *key; ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) { + if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) { + val = Z_REFVAL_P(val); + } + Z_TRY_ADDREF_P(val); if (key) { - zend_throw_error(NULL, "Cannot unpack array with string keys"); - FREE_OP(opline->op1_type, opline->op1.var); - HANDLE_EXCEPTION(); + zend_hash_update(result_ht, key, val); } else { - if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) { - val = Z_REFVAL_P(val); - } - Z_TRY_ADDREF_P(val); - if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) { + if (!zend_hash_next_index_insert(result_ht, val)) { zend_cannot_add_element(); zval_ptr_dtor_nogc(val); break; @@ -2560,32 +2560,42 @@ add_unpack_again: break; } + zval key; if (iter->funcs->get_current_key) { - zval key; iter->funcs->get_current_key(iter, &key); if (UNEXPECTED(EG(exception) != NULL)) { break; } - if (UNEXPECTED(Z_TYPE(key) != IS_LONG)) { + if (UNEXPECTED(Z_TYPE(key) != IS_LONG && Z_TYPE(key) != IS_STRING)) { zend_throw_error(NULL, - (Z_TYPE(key) == IS_STRING) ? - "Cannot unpack Traversable with string keys" : - "Cannot unpack Traversable with non-integer keys"); + "Keys must be of type int|string during array unpacking"); zval_ptr_dtor(&key); break; } + } else { + ZVAL_UNDEF(&key); } ZVAL_DEREF(val); Z_TRY_ADDREF_P(val); - if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) { - zend_cannot_add_element(); - zval_ptr_dtor_nogc(val); + zend_ulong num_key; + if (Z_TYPE(key) == IS_STRING && !ZEND_HANDLE_NUMERIC(Z_STR(key), num_key)) { + zend_hash_update(result_ht, Z_STR(key), val); + zval_ptr_dtor_str(&key); + } else { + if (!zend_hash_next_index_insert(result_ht, val)) { + zend_cannot_add_element(); + zval_ptr_dtor_nogc(val); + break; + } } iter->funcs->move_forward(iter); + if (UNEXPECTED(EG(exception))) { + break; + } } zend_iterator_dtor(iter); -- cgit v1.2.1