diff --git a/NEWS b/NEWS index cc86611e0a3f..44e23edcd531 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 05ae834590c6..727bf1221e0b 100644 --- a/UPGRADING +++ b/UPGRADING @@ -95,6 +95,13 @@ 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. The values are + still released after the last assignment, as the array was. + - 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 000000000000..92e0e096b9ad --- /dev/null +++ b/Zend/tests/list/list_from_array_literal.phpt @@ -0,0 +1,97 @@ +--TEST-- +List assignment from an array literal: values and targets +--FILE-- +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); + +$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) +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 000000000000..534842bdc22f --- /dev/null +++ b/Zend/tests/list/list_from_array_literal_order.inc @@ -0,0 +1,116 @@ +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 lives until the assignments are done\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 during the release after the assignments\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 000000000000..b81c188997e4 --- /dev/null +++ b/Zend/tests/list/list_from_array_literal_order.phpt @@ -0,0 +1,88 @@ +--TEST-- +List assignment from an array literal: evaluation order and lifetime of the values +--FILE-- + +--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 lives until the assignments are done +construct k +construct l +set k +set l +destruct k +destruct l +assigned +construct old b +construct first +construct second +destruct old b +destruct first +assigned +destruct second +-- the same with a destructor that throws +boom +int(42) +-- two targets that are references to each other +boom +int(42) +-- a variable that is a reference to a typed property +construct coerced +set y +destruct coerced +assigned +-- pending values when a target expression throws +construct v1 +construct v2 +construct v3 +destruct v2 +destruct v3 +destruct v1 +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 during the release after the assignments +construct a +construct b +set a +set b +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 000000000000..de66608dff62 --- /dev/null +++ b/Zend/tests/list/list_from_array_literal_order_opt.phpt @@ -0,0 +1,94 @@ +--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-- + +--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 lives until the assignments are done +construct k +construct l +set k +set l +destruct k +destruct l +assigned +construct old b +construct first +construct second +destruct old b +destruct first +assigned +destruct second +-- the same with a destructor that throws +boom +int(42) +-- two targets that are references to each other +boom +int(42) +-- a variable that is a reference to a typed property +construct coerced +set y +destruct coerced +assigned +-- pending values when a target expression throws +construct v1 +construct v2 +construct v3 +destruct v2 +destruct v3 +destruct v1 +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 during the release after the assignments +construct a +construct b +set a +set b +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 000000000000..9898e37f7780 --- /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-- + +--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 b994f3251674..16bc4794f1bb 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3532,6 +3532,82 @@ 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, a variable is copied before any assignment, each target is + * assigned a copy of its value and the values are freed after all assignments, as the array was */ +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++) { + znode *value = &value_nodes[i]; + zend_ast *target_ast = targets->child[i]->child[0]; + znode copy; + + zend_verify_list_assign_target(target_ast, var_ast->attr); + if (value->op_type == IS_CONST) { + copy = *value; + value->op_type = IS_UNUSED; + } else { + zend_emit_op_tmp(©, ZEND_COPY_TMP, value, NULL); + } + zend_emit_assign_znode(target_ast, ©); + } + + for (uint32_t i = 0; i < count; i++) { + if (value_nodes[i].op_type != IS_UNUSED) { + zend_do_free(&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]; @@ -3608,6 +3684,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 +6660,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 +6682,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 +6701,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 +6712,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 000000000000..aeb6becb8cba --- /dev/null +++ b/ext/opcache/tests/opt/list_assign_from_array_literal.phpt @@ -0,0 +1,107 @@ +--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-- + 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=12, args=2, vars=2, tmps=6) + ; (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 T4 = COPY_TMP T2 +0005 ASSIGN CV0($a) T4 +0006 T6 = COPY_TMP T3 +0007 ASSIGN CV1($b) T6 +0008 FREE T2 +0009 FREE T3 +0010 RETURN CV0($a) +0011 RETURN null + +fibonacci: + ; (lines=17, args=1, vars=3, tmps=10) + ; (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 0013 +0004 PRE_DEC CV0($n) +0005 T6 = QM_ASSIGN CV2($b) +0006 T7 = ADD CV1($a) CV2($b) +0007 T8 = COPY_TMP T6 +0008 ASSIGN CV1($a) T8 +0009 T10 = COPY_TMP T7 +0010 ASSIGN CV2($b) T10 +0011 FREE T6 +0012 FREE T7 +0013 T12 = IS_SMALLER int(0) CV0($n) +0014 JMPNZ T12 0004 +0015 RETURN CV1($a) +0016 RETURN null + +elements: + ; (lines=13, args=1, vars=1, tmps=6) + ; (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 T3 = COPY_TMP T1 +0004 ASSIGN_DIM CV0($p) int(0) +0005 OP_DATA T3 +0006 T5 = COPY_TMP T2 +0007 ASSIGN_DIM CV0($p) int(1) +0008 OP_DATA T5 +0009 FREE T1 +0010 FREE T2 +0011 RETURN CV0($p) +0012 RETURN null + +twice: + ; (lines=8, args=1, vars=1, tmps=4) + ; (after optimizer) + ; %slist_assign_from_array_literal.php:14-17 +0000 CV0($x) = RECV 1 +0001 T1 = QM_ASSIGN CV0($x) +0002 T2 = COPY_TMP T1 +0003 ASSIGN CV0($x) T2 +0004 ASSIGN CV0($x) int(2) +0005 FREE T1 +0006 RETURN CV0($x) +0007 RETURN null