Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions Zend/tests/array_addition_empty_operand.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
--TEST--
Array addition with an empty operand
--FILE--
<?php

function show(string $label, array $result): void
{
$current = key($result);
$result[-5] = 'negative';
$result[] = 'appended';
echo $label, ': current=', json_encode($current), ' keys=', json_encode(array_keys($result)), PHP_EOL;
}

$empty = [];

echo '-- empty right operand', PHP_EOL;
$hash = ['x' => 1, 'y' => 2];
show('hash', $hash + $empty);
next($hash);
show('hash, moved pointer', $hash + $empty);
next($hash);
show('hash, exhausted pointer', $hash + $empty);
$list = [1, 2, 3];
unset($list[2]);
show('list, stale next index', $list + $empty);
$emptied = [1];
unset($emptied[0]);
show('emptied list', $emptied + $empty);

echo '-- empty left operand', PHP_EOL;
$hash = ['x' => 1, 'y' => 2];
show('hash', $empty + $hash);
next($hash);
show('hash, moved pointer', $empty + $hash);
$hash = ['x' => 1, 5 => 2];
show('hash, integer key', $empty + $hash);
unset($hash[5]);
show('hash, removed integer key', $empty + $hash);
$list = [1, 2, 3];
show('list', $empty + $list);
next($list);
show('list, moved pointer', $empty + $list);
unset($list[2]);
show('list, stale next index', $empty + $list);
$list = [1, 2, 3];
unset($list[0]);
show('list, leading hole', $empty + $list);
show('emptied list on the left', $emptied + ['x' => 1]);

echo '-- both empty', PHP_EOL;
$popped = [1];
array_pop($popped);
show('empty + popped', $empty + $popped);
show('popped + empty', $popped + $empty);
show('empty + emptied', $empty + $emptied);
show('emptied + empty', $emptied + $empty);

echo '-- compound assignment', PHP_EOL;
$target = [];
$target += $popped;
show('empty += popped', $target);
$target = [];
$target += ['x' => 1, 'y' => 2];
show('empty += hash', $target);
$target = ['x' => 1];
$target += $empty;
show('hash += empty', $target);

echo '-- the operands are left untouched', PHP_EOL;
const DEFAULTS = ['a' => 1, 'b' => 2];
$result = $empty + DEFAULTS;
$result['c'] = 3;
$other = DEFAULTS + $empty;
$other['c'] = 3;
$target = [];
$target += DEFAULTS;
$target['c'] = 3;
echo json_encode(DEFAULTS), ' ', json_encode($result), ' ', json_encode($other), ' ', json_encode($target), PHP_EOL;

$source = ['k' => 'v'];
$source['l'] = 'w';
$copy = $source;
$source += $empty;
$source['m'] = 'x';
$result = $empty + $copy;
$result['k'] = 'changed';
echo json_encode($copy), ' ', json_encode($source), ' ', json_encode($result), PHP_EOL;

$value = 1;
$withReference = ['r' => &$value];
$result = $empty + $withReference;
$result['r'] = 2;
echo $value, PHP_EOL;

?>
--EXPECT--
-- empty right operand
hash: current="x" keys=["x","y",-5,-4]
hash, moved pointer: current="y" keys=["x","y",-5,-4]
hash, exhausted pointer: current="x" keys=["x","y",-5,-4]
list, stale next index: current=0 keys=[0,1,-5,3]
emptied list: current=null keys=[-5,1]
-- empty left operand
hash: current="x" keys=["x","y",-5,-4]
hash, moved pointer: current="x" keys=["x","y",-5,-4]
hash, integer key: current="x" keys=["x",5,-5,6]
hash, removed integer key: current="x" keys=["x",-5,-4]
list: current=0 keys=[0,1,2,-5,3]
list, moved pointer: current=0 keys=[0,1,2,-5,3]
list, stale next index: current=0 keys=[0,1,-5,2]
list, leading hole: current=1 keys=[1,2,-5,3]
emptied list on the left: current="x" keys=["x",-5,1]
-- both empty
empty + popped: current=null keys=[-5,-4]
popped + empty: current=null keys=[-5,0]
empty + emptied: current=null keys=[-5,-4]
emptied + empty: current=null keys=[-5,1]
-- compound assignment
empty += popped: current=null keys=[-5,-4]
empty += hash: current="x" keys=["x","y",-5,-4]
hash += empty: current="x" keys=["x",-5,-4]
-- the operands are left untouched
{"a":1,"b":2} {"a":1,"b":2,"c":3} {"a":1,"b":2,"c":3} {"a":1,"b":2,"c":3}
{"k":"v","l":"w"} {"k":"v","l":"w","m":"x"} {"k":"changed","l":"w"}
2
28 changes: 28 additions & 0 deletions Zend/zend_operators.c
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,34 @@ static zend_never_inline void ZEND_FASTCALL add_function_array(zval *result, con
/* $a += $a */
return;
}
if (zend_hash_num_elements(Z_ARRVAL_P(op2)) == 0 &&
Comment thread
ndossche marked this conversation as resolved.
Z_ARRVAL_P(op1)->nInternalPointer < Z_ARRVAL_P(op1)->nNumUsed) {
/* $a += [] or $a + [], unless the internal pointer is past the
* end as zend_array_dup() would reset it */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments make it sound like the internal pointer is the only thing that changes, which is misleading

if (result != op1) {
ZVAL_COPY(result, op1);
}
return;
}
if (zend_hash_num_elements(Z_ARRVAL_P(op1)) == 0 &&
zend_hash_num_elements(Z_ARRVAL_P(op2)) != 0) {
/* [] + $a or $a += $b with an empty $a, $b is shared only if a
* merge would give the same array: op1 never had an integer key,
* $b has its pointer on the first element and its next free
* index is not stale */
const HashTable *src = Z_ARRVAL_P(op2);
zend_long next_free = HT_IS_PACKED(src) ? (zend_long) src->nNumUsed : ZEND_LONG_MIN;
if (Z_ARRVAL_P(op1)->nNextFreeElement == ZEND_LONG_MIN &&
src->nInternalPointer == 0 &&
src->nNextFreeElement == next_free) {
zend_array *old = result == op1 ? Z_ARR_P(result) : NULL;
ZVAL_COPY(result, op2);
if (old) {
zend_array_release(old);
}
return;
}
}
if (result != op1) {
ZVAL_ARR(result, zend_array_dup(Z_ARR_P(op1)));
} else {
Expand Down
Loading