Commit 6277778f08f for php
commit 6277778f08fec7eca0ba1eca2cb271c9955f7dea
Author: Artem Ukrainskiy <art.ukrainskiy@gmail.com>
Date: Mon Oct 19 12:48:28 2026 +0300
Zend: Compile a list assignment from an array literal without the array (#24180)
[$a, $b] = [$b, $a]; built an array of the values, fetched every element
out of it again and freed it. When the result of the assignment is not
used, the right side is an array literal and the left side a flat list
with a target for every value, assign the values to the targets directly
instead.
The values are still evaluated in order before the first assignment and
a variable among them is copied first. Each target then takes its value,
so a value its target does not keep (a setter that drops it, the same
variable twice, a typed property that coerces it) is released at that
assignment rather than after the last one, and a destructor that throws
there stops the rest of the statement.
Assignments in the expression lists of a for loop, whose results are not
used either, are compiled the same way. Nested lists, keys on either
side, references, spread, a different number of values and targets and
a used result keep the usual compilation.
A swap goes from 922 to 467 instructions per call of a function doing
it, below a swap through a temporary variable (493).
diff --git a/NEWS b/NEWS
index faee75f6942..5240b0c6d1d 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,11 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.7.0alpha1
+- Core:
+ . A flat unkeyed list assignment from an array literal with one value per
+ target, such as [$a, $b] = [$b, $a], no longer builds the array when the
+ result is unused (GH-23048). (Artem Ukrainskiy)
+
- Curl:
. curl_multi_exec() now throws a TypeError when still_running is not an int.
(arshidkv12)
diff --git a/UPGRADING b/UPGRADING
index 0c3616dec07..fed93f01d56 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -122,6 +122,14 @@ PHP 8.7 UPGRADE NOTES
14. Performance Improvements
========================================
+- Core:
+ . A flat unkeyed list assignment from an array literal with one value per
+ target, such as [$a, $b] = [$b, $a], no longer builds the array when the
+ result is unused: the values are evaluated in order, a variable among them
+ is copied, and they are assigned to the targets directly. A value its
+ target does not keep is now released at that assignment instead of after
+ the last one.
+
- Standard:
. Improved performance of strcspn() and strpbrk() when searching longer strings
with a single-byte character mask.
diff --git a/Zend/tests/list/list_from_array_literal.phpt b/Zend/tests/list/list_from_array_literal.phpt
new file mode 100644
index 00000000000..9c3fbddbe81
--- /dev/null
+++ b/Zend/tests/list/list_from_array_literal.phpt
@@ -0,0 +1,110 @@
+--TEST--
+List assignment from an array literal: values and targets
+--FILE--
+<?php
+
+$a = 1; $b = 2; $c = 3;
+[$a, $b] = [$b, $a];
+var_dump($a, $b);
+
+[$a, $b, $c] = [$b, $c, $a];
+var_dump($a, $b, $c);
+
+list($a, $b) = array($b, $a);
+var_dump($a, $b);
+
+[$a, $a] = [1, 2];
+var_dump($a);
+
+$a = 1;
+[$a, $b] = [2, $a];
+var_dump($a, $b);
+
+[$a, $b,] = [$b, $a,];
+var_dump($a, $b);
+
+class P { public $a = 1; public $b = 2; public static $c = 3; public static $d = 4; }
+$p = new P;
+[$p->a, $p->b] = [$p->b, $p->a];
+[P::$c, P::$d] = [P::$d, P::$c];
+var_dump($p->a, $p->b, P::$c, P::$d);
+
+$x = [1, 2];
+[$x[0], $x[1]] = [$x[1], $x[0]];
+var_dump($x);
+
+[$GLOBALS['g']] = [5];
+var_dump($g);
+
+$a = 1; $b = 2;
+[$a, $b] = [1 => 'one', 0 => 'zero'];
+var_dump($a, $b);
+
+$a = 1; $b = 2;
+[[$a, $b], $c] = [[$b, $a], 3];
+var_dump($a, $b, $c);
+
+$r = [$a, $b] = [7, 8];
+var_dump($r, $a, $b);
+
+$a = 1;
+[$f, $a] = [fn() => $a, 2];
+var_dump($f());
+
+for ([$a, $b] = [1, 2], $i = 0; $i < 3; $i++, [$a, $b] = [$b, $a + $b]) {
+ echo "$a $b\n";
+}
+for ($i = 0; [$a, $b] = [$b, $a], $i < 2; $i++) {
+ echo "$a $b\n";
+}
+for ($i = 0; [$x, $y] = [$i, $i * 2]; $i++) {
+ if ($i > 1) break;
+ echo "$x $y\n";
+}
+
+?>
+--EXPECT--
+int(2)
+int(1)
+int(1)
+int(3)
+int(2)
+int(3)
+int(1)
+int(2)
+int(2)
+int(1)
+int(1)
+int(2)
+int(2)
+int(1)
+int(4)
+int(3)
+array(2) {
+ [0]=>
+ int(2)
+ [1]=>
+ int(1)
+}
+int(5)
+string(4) "zero"
+string(3) "one"
+int(2)
+int(1)
+int(3)
+array(2) {
+ [0]=>
+ int(7)
+ [1]=>
+ int(8)
+}
+int(7)
+int(8)
+int(1)
+1 2
+2 3
+3 5
+8 5
+5 8
+0 0
+1 2
diff --git a/Zend/tests/list/list_from_array_literal_order.inc b/Zend/tests/list/list_from_array_literal_order.inc
new file mode 100644
index 00000000000..0331ff015bb
--- /dev/null
+++ b/Zend/tests/list/list_from_array_literal_order.inc
@@ -0,0 +1,116 @@
+<?php
+
+class D {
+ public function __construct(public string $n) { echo "construct $this->n\n"; }
+ public function __destruct() { echo "destruct $this->n\n"; $GLOBALS['dead'] = true; }
+ public function __toString(): string { return $this->n; }
+}
+function v(string $n) { echo "value $n\n"; return $n; }
+function k(string $n) { echo "key $n\n"; return 0; }
+
+echo "-- values before targets\n";
+$x = [];
+[$x[k('a')], $x[k('b')]] = [v('1'), v('2')];
+
+echo "-- extra values\n";
+[$a] = [new D('a'), new D('x'), new D('y')];
+echo "assigned\n";
+unset($a);
+
+echo "-- values nobody takes live until the assignments are done\n";
+function untaken_variable() {
+ $x = new D('old x');
+ $y = new D('y');
+ [, $x, $y] = [$x, 1, 2];
+ echo "assigned\n";
+}
+untaken_variable();
+
+echo "-- a value the target does not keep is released at its assignment\n";
+final class Drop implements ArrayAccess {
+ public function offsetExists($o): bool { return true; }
+ public function offsetGet($o): mixed { return null; }
+ public function offsetSet($o, $v): void { echo "set $o\n"; }
+ public function offsetUnset($o): void {}
+}
+$drop = new Drop;
+[$drop['k'], $drop['l']] = [new D('k'), new D('l')];
+echo "assigned\n";
+$b = new D('old b');
+[$a, $a, $b] = [new D('first'), new D('second'), 3];
+echo "assigned\n";
+unset($a);
+
+echo "-- the same with a destructor that throws\n";
+final class Boom { public function __destruct() { throw new Exception('boom'); } }
+$b = 0;
+try {
+ [$a, $a, $b] = [new Boom, new stdClass, 42];
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+var_dump($b);
+unset($a);
+
+echo "-- two targets that are references to each other\n";
+$a = null; $b = null; $c = 0;
+$a = &$b;
+try {
+ [$a, $b, $c] = [new Boom, new stdClass, 42];
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+var_dump($c);
+unset($a, $b);
+
+echo "-- a variable that is a reference to a typed property\n";
+final class S { public string $s = ''; }
+$obj = new S;
+$r = &$obj->s;
+[$r, $drop['y']] = [new D('coerced'), 1];
+echo "assigned\n";
+unset($r, $obj);
+
+echo "-- pending values when a target expression throws\n";
+function boom() { throw new Exception('boom'); }
+try {
+ [$arr[boom()], $b, $c] = [new D('v1'), new D('v2'), new D('v3')];
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "-- a variable is copied before any assignment\n";
+$o = new D('o');
+[$o, $p] = [new D('new o'), $o];
+echo "assigned\n";
+var_dump($o->n, $p->n);
+unset($o, $p);
+
+echo "-- exception in a later value\n";
+try {
+ [$a, $b, $c] = [new D('first'), new D('second'), boom()];
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "-- exception in an assignment\n";
+class T { public int $i = 1; }
+$t = new T;
+try {
+ [$t->i, $b] = ['not an int', new D('after the throw')];
+} catch (TypeError $e) {
+ echo get_class($e), "\n";
+}
+var_dump($t->i);
+
+echo "-- a destructor that throws when a target drops the value\n";
+final class BoomNamed extends D {
+ public function __destruct() { echo "destruct $this->n throws\n"; throw new Exception("boom $this->n"); }
+}
+try {
+ [$drop['a'], $drop['b']] = [new BoomNamed('a'), new D('b')];
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "done\n";
diff --git a/Zend/tests/list/list_from_array_literal_order.phpt b/Zend/tests/list/list_from_array_literal_order.phpt
new file mode 100644
index 00000000000..db8f9e71d54
--- /dev/null
+++ b/Zend/tests/list/list_from_array_literal_order.phpt
@@ -0,0 +1,87 @@
+--TEST--
+List assignment from an array literal: evaluation order and lifetime of the values
+--FILE--
+<?php
+require __DIR__ . '/list_from_array_literal_order.inc';
+?>
+--EXPECT--
+-- values before targets
+value 1
+value 2
+key a
+key b
+-- extra values
+construct a
+construct x
+construct y
+destruct x
+destruct y
+assigned
+destruct a
+-- values nobody takes live until the assignments are done
+construct old x
+construct y
+destruct y
+destruct old x
+assigned
+-- a value the target does not keep is released at its assignment
+construct k
+construct l
+set k
+destruct k
+set l
+destruct l
+assigned
+construct old b
+construct first
+construct second
+destruct first
+destruct old b
+assigned
+destruct second
+-- the same with a destructor that throws
+boom
+int(0)
+-- two targets that are references to each other
+boom
+int(0)
+-- a variable that is a reference to a typed property
+construct coerced
+destruct coerced
+set y
+assigned
+-- pending values when a target expression throws
+construct v1
+construct v2
+construct v3
+destruct v1
+destruct v2
+destruct v3
+boom
+-- a variable is copied before any assignment
+construct o
+construct new o
+assigned
+string(5) "new o"
+string(1) "o"
+destruct new o
+destruct o
+-- exception in a later value
+construct first
+construct second
+destruct first
+destruct second
+boom
+-- exception in an assignment
+construct after the throw
+destruct after the throw
+TypeError
+int(1)
+-- a destructor that throws when a target drops the value
+construct a
+construct b
+set a
+destruct a throws
+destruct b
+boom a
+done
diff --git a/Zend/tests/list/list_from_array_literal_order_opt.phpt b/Zend/tests/list/list_from_array_literal_order_opt.phpt
new file mode 100644
index 00000000000..110032797ae
--- /dev/null
+++ b/Zend/tests/list/list_from_array_literal_order_opt.phpt
@@ -0,0 +1,93 @@
+--TEST--
+List assignment from an array literal: evaluation order and lifetime of the values, with the optimizer
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+--FILE--
+<?php
+require __DIR__ . '/list_from_array_literal_order.inc';
+?>
+--EXPECT--
+-- values before targets
+value 1
+value 2
+key a
+key b
+-- extra values
+construct a
+construct x
+construct y
+destruct x
+destruct y
+assigned
+destruct a
+-- values nobody takes live until the assignments are done
+construct old x
+construct y
+destruct y
+destruct old x
+assigned
+-- a value the target does not keep is released at its assignment
+construct k
+construct l
+set k
+destruct k
+set l
+destruct l
+assigned
+construct old b
+construct first
+construct second
+destruct first
+destruct old b
+assigned
+destruct second
+-- the same with a destructor that throws
+boom
+int(0)
+-- two targets that are references to each other
+boom
+int(0)
+-- a variable that is a reference to a typed property
+construct coerced
+destruct coerced
+set y
+assigned
+-- pending values when a target expression throws
+construct v1
+construct v2
+construct v3
+destruct v1
+destruct v2
+destruct v3
+boom
+-- a variable is copied before any assignment
+construct o
+construct new o
+assigned
+string(5) "new o"
+string(1) "o"
+destruct new o
+destruct o
+-- exception in a later value
+construct first
+construct second
+destruct first
+destruct second
+boom
+-- exception in an assignment
+construct after the throw
+destruct after the throw
+TypeError
+int(1)
+-- a destructor that throws when a target drops the value
+construct a
+construct b
+set a
+destruct a throws
+destruct b
+boom a
+done
diff --git a/Zend/tests/list/list_from_array_literal_undefined.phpt b/Zend/tests/list/list_from_array_literal_undefined.phpt
new file mode 100644
index 00000000000..9898e37f778
--- /dev/null
+++ b/Zend/tests/list/list_from_array_literal_undefined.phpt
@@ -0,0 +1,31 @@
+--TEST--
+List assignment from an array literal: undefined variables
+--FILE--
+<?php
+
+function f() {
+ [$a, $b] = [$u0, $u1];
+ var_dump($a, $b);
+
+ [$a,
+ $b] = [
+ $u2,
+ $u3];
+ var_dump($a, $b);
+}
+f();
+
+?>
+--EXPECTF--
+
+Warning: Undefined variable $u0 in %s on line 4
+
+Warning: Undefined variable $u1 in %s on line 4
+NULL
+NULL
+
+Warning: Undefined variable $u2 in %s on line 9
+
+Warning: Undefined variable $u3 in %s on line 10
+NULL
+NULL
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index f33ae242fee..020e8bbe631 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -3516,6 +3516,68 @@ static void zend_compile_expr_with_potential_assign_to_self(
}
}
+/* Flat unkeyed list and literal without references or spread, one value per target */
+static bool zend_list_assign_from_array_applies(zend_ast *var_ast, zend_ast *expr_ast) /* {{{ */
+{
+ if (expr_ast->kind != ZEND_AST_ARRAY) {
+ return false;
+ }
+
+ const zend_ast_list *targets = zend_ast_get_list(var_ast);
+ const zend_ast_list *values = zend_ast_get_list(expr_ast);
+ if (targets->children == 0 || values->children != targets->children) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < targets->children; i++) {
+ const zend_ast *target_ast = targets->child[i];
+ const zend_ast *value_ast = values->child[i];
+ if (target_ast == NULL || target_ast->kind != ZEND_AST_ARRAY_ELEM || target_ast->child[1] != NULL || target_ast->attr
+ || target_ast->child[0]->kind == ZEND_AST_ARRAY) {
+ return false;
+ }
+ if (value_ast == NULL || value_ast->kind != ZEND_AST_ARRAY_ELEM || value_ast->child[1] != NULL || value_ast->attr) {
+ return false;
+ }
+ }
+ return true;
+}
+/* }}} */
+
+/* [$a, $b] = [$b, $a]; with an unused result: assigns the values directly, without the array.
+ * The values are compiled in order and a variable is copied before any assignment; each target
+ * then takes its value */
+static bool zend_try_compile_list_assign_from_array(zend_ast *var_ast, zend_ast *expr_ast) /* {{{ */
+{
+ if (!zend_list_assign_from_array_applies(var_ast, expr_ast)) {
+ return false;
+ }
+
+ const zend_ast_list *targets = zend_ast_get_list(var_ast);
+ const zend_ast_list *values = zend_ast_get_list(expr_ast);
+ uint32_t count = values->children;
+ znode *value_nodes = safe_emalloc(count, sizeof(znode), 0);
+
+ for (uint32_t i = 0; i < count; i++) {
+ znode *value = &value_nodes[i];
+ zend_compile_expr(value, values->child[i]->child[0]);
+ if (value->op_type == IS_CV) {
+ zend_emit_op_tmp(value, ZEND_QM_ASSIGN, value, NULL);
+ }
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ zend_ast *target_ast = targets->child[i]->child[0];
+
+ zend_verify_list_assign_target(target_ast, var_ast->attr);
+ zend_emit_assign_znode(target_ast, &value_nodes[i]);
+ }
+
+ efree(value_nodes);
+ return true;
+}
+/* }}} */
+
static void zend_compile_assign(znode *result, zend_ast *ast, bool stmt, uint32_t type) /* {{{ */
{
zend_ast *var_ast = ast->child[0];
@@ -3592,6 +3654,10 @@ static void zend_compile_assign(znode *result, zend_ast *ast, bool stmt, uint32_
* self-assignments, this forces the RHS to evaluate first. */
zend_emit_op(&expr_node, ZEND_MAKE_REF, &expr_node, NULL);
} else {
+ if (stmt && zend_try_compile_list_assign_from_array(var_ast, expr_ast)) {
+ result->op_type = IS_UNUSED;
+ return;
+ }
if (expr_ast->kind == ZEND_AST_VAR) {
/* list($a, $b) = $a should evaluate the right $a first */
znode cv_node;
@@ -6580,7 +6646,8 @@ static void zend_compile_do_while(const zend_ast *ast) /* {{{ */
}
/* }}} */
-static void zend_compile_for_expr_list(znode *result, zend_ast *ast) /* {{{ */
+/* An assignment whose result is unused is compiled as a statement */
+static void zend_compile_for_expr_list(znode *result, zend_ast *ast, bool last_result_used) /* {{{ */
{
const zend_ast_list *list;
uint32_t i;
@@ -6601,6 +6668,8 @@ static void zend_compile_for_expr_list(znode *result, zend_ast *ast) /* {{{ */
zend_compile_void_cast(NULL, expr_ast);
result->op_type = IS_CONST;
ZVAL_NULL(&result->u.constant);
+ } else if (expr_ast->kind == ZEND_AST_ASSIGN && (i + 1 < list->children || !last_result_used)) {
+ zend_compile_assign(result, expr_ast, /* stmt */ true, BP_VAR_R);
} else {
zend_compile_expr(result, expr_ast);
}
@@ -6618,7 +6687,7 @@ static void zend_compile_for(const zend_ast *ast) /* {{{ */
znode result;
uint32_t opnum_start, opnum_jmp, opnum_loop;
- zend_compile_for_expr_list(&result, init_ast);
+ zend_compile_for_expr_list(&result, init_ast, false);
zend_do_free(&result);
opnum_jmp = zend_emit_jump(0);
@@ -6629,11 +6698,11 @@ static void zend_compile_for(const zend_ast *ast) /* {{{ */
zend_compile_stmt(stmt_ast);
opnum_loop = get_next_op_number();
- zend_compile_for_expr_list(&result, loop_ast);
+ zend_compile_for_expr_list(&result, loop_ast, false);
zend_do_free(&result);
zend_update_jump_target_to_next(opnum_jmp);
- zend_compile_for_expr_list(&result, cond_ast);
+ zend_compile_for_expr_list(&result, cond_ast, true);
zend_do_extended_stmt(NULL);
zend_emit_cond_jump(ZEND_JMPNZ, &result, opnum_start);
diff --git a/ext/opcache/tests/opt/list_assign_from_array_literal.phpt b/ext/opcache/tests/opt/list_assign_from_array_literal.phpt
new file mode 100644
index 00000000000..5a586c61874
--- /dev/null
+++ b/ext/opcache/tests/opt/list_assign_from_array_literal.phpt
@@ -0,0 +1,93 @@
+--TEST--
+List assignment from an array literal compiles without the array
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=0
+opcache.opt_debug_level=0x20000
+opcache.preload=
+zend_test.observer.enabled=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+function swap($a, $b) {
+ [$a, $b] = [$b, $a];
+ return $a;
+}
+function fibonacci($n) {
+ for ($a = 0, $b = 1; $n > 0; $n--, [$a, $b] = [$b, $a + $b]);
+ return $a;
+}
+function elements($p) {
+ [$p[0], $p[1]] = [$p[1], $p[0]];
+ return $p;
+}
+function twice($x) {
+ [$x, $x] = [$x, 2];
+ return $x;
+}
+?>
+--EXPECTF--
+
+$_main:
+ ; (lines=1, args=0, vars=0, tmps=0)
+ ; (after optimizer)
+ ; %slist_assign_from_array_literal.php:1-19
+0000 RETURN int(1)
+
+swap:
+ ; (lines=8, args=2, vars=2, tmps=4)
+ ; (after optimizer)
+ ; %slist_assign_from_array_literal.php:2-5
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 T2 = QM_ASSIGN CV1($b)
+0003 T3 = QM_ASSIGN CV0($a)
+0004 ASSIGN CV0($a) T2
+0005 ASSIGN CV1($b) T3
+0006 RETURN CV0($a)
+0007 RETURN null
+
+fibonacci:
+ ; (lines=13, args=1, vars=3, tmps=8)
+ ; (after optimizer)
+ ; %slist_assign_from_array_literal.php:6-9
+0000 CV0($n) = RECV 1
+0001 ASSIGN CV1($a) int(0)
+0002 ASSIGN CV2($b) int(1)
+0003 JMP 0009
+0004 PRE_DEC CV0($n)
+0005 T6 = QM_ASSIGN CV2($b)
+0006 T7 = ADD CV1($a) CV2($b)
+0007 ASSIGN CV1($a) T6
+0008 ASSIGN CV2($b) T7
+0009 T10 = IS_SMALLER int(0) CV0($n)
+0010 JMPNZ T10 0004
+0011 RETURN CV1($a)
+0012 RETURN null
+
+elements:
+ ; (lines=9, args=1, vars=1, tmps=4)
+ ; (after optimizer)
+ ; %slist_assign_from_array_literal.php:10-13
+0000 CV0($p) = RECV 1
+0001 T1 = FETCH_DIM_R CV0($p) int(1)
+0002 T2 = FETCH_DIM_R CV0($p) int(0)
+0003 ASSIGN_DIM CV0($p) int(0)
+0004 OP_DATA T1
+0005 ASSIGN_DIM CV0($p) int(1)
+0006 OP_DATA T2
+0007 RETURN CV0($p)
+0008 RETURN null
+
+twice:
+ ; (lines=6, args=1, vars=1, tmps=3)
+ ; (after optimizer)
+ ; %slist_assign_from_array_literal.php:14-17
+0000 CV0($x) = RECV 1
+0001 T1 = QM_ASSIGN CV0($x)
+0002 ASSIGN CV0($x) T1
+0003 ASSIGN CV0($x) int(2)
+0004 RETURN CV0($x)
+0005 RETURN null
\ No newline at end of file