Always memoize calls in lhs of coalesce assignment

We don't want to invoke calls twice, even if they are considered "variables",
i.e. might be writable if returning a reference. Function calls behave the same
in all BP contexts so they don't need to be invoked twice. The singular
exception to this is nullsafe coalesce in isset/empty, because it needs to
return false/true respectively when short-circuited. However, since nullsafe
calls are not allwed in write context we may ignore this problem.

Closes GH-11592
This commit is contained in:
Ilija Tovilo 2023-07-04 14:16:35 +02:00
parent c0ce3e7efa
commit 1057cce1c0
No known key found for this signature in database
GPG Key ID: A4F5D403F118200A
2 changed files with 77 additions and 16 deletions

View File

@ -0,0 +1,64 @@
--TEST--
Assign coalesce: All calls should be memoized
--FILE--
<?php
class Foo {
public $prop;
public function foo() {
echo __METHOD__, "\n";
return $this;
}
public function bar() {
echo __METHOD__, "\n";
return 'prop';
}
public function __isset($name) {
echo __METHOD__, "\n";
return false;
}
public function __set($name, $value) {
echo __METHOD__, "\n";
var_dump($value);
}
}
function &foo() {
global $foo;
echo __FUNCTION__, "\n";
return $foo;
}
function bar() {
echo __FUNCTION__, "\n";
}
foo(bar())['bar'] ??= 42;
var_dump($foo);
$foo = new Foo();
$foo->foo()->foo()->{$foo->bar()} ??= 42;
var_dump($foo);
$foo->foo()->baz ??= 42;
?>
--EXPECT--
bar
foo
array(1) {
["bar"]=>
int(42)
}
Foo::foo
Foo::foo
Foo::bar
object(Foo)#1 (1) {
["prop"]=>
int(42)
}
Foo::foo
Foo::__isset
Foo::__set
int(42)

View File

@ -4589,14 +4589,7 @@ static void zend_compile_call(znode *result, zend_ast *ast, uint32_t type) /* {{
if (runtime_resolution) {
if (zend_string_equals_literal_ci(zend_ast_get_str(name_ast), "assert")
&& !is_callable_convert) {
if (CG(memoize_mode) == ZEND_MEMOIZE_NONE) {
zend_compile_assert(result, zend_ast_get_list(args_ast), Z_STR(name_node.u.constant), NULL, ast->lineno);
} else {
/* We want to always memoize assert calls, even if they are positioned in
* write-context. This prevents memoizing their arguments that might not be
* evaluated if assertions are disabled, using a TMPVAR that wasn't initialized. */
zend_compile_memoized_expr(result, ast);
}
zend_compile_assert(result, zend_ast_get_list(args_ast), Z_STR(name_node.u.constant), NULL, ast->lineno);
} else {
zend_compile_ns_call(result, &name_node, args_ast, ast->lineno);
}
@ -4615,14 +4608,7 @@ static void zend_compile_call(znode *result, zend_ast *ast, uint32_t type) /* {{
/* Special assert() handling should apply independently of compiler flags. */
if (fbc && zend_string_equals_literal(lcname, "assert") && !is_callable_convert) {
if (CG(memoize_mode) == ZEND_MEMOIZE_NONE) {
zend_compile_assert(result, zend_ast_get_list(args_ast), lcname, fbc, ast->lineno);
} else {
/* We want to always memoize assert calls, even if they are positioned in
* write-context. This prevents memoizing their arguments that might not be
* evaluated if assertions are disabled, using a TMPVAR that wasn't initialized. */
zend_compile_memoized_expr(result, ast);
}
zend_compile_assert(result, zend_ast_get_list(args_ast), lcname, fbc, ast->lineno);
zend_string_release(lcname);
zval_ptr_dtor(&name_node.u.constant);
return;
@ -10591,6 +10577,17 @@ static zend_op *zend_compile_var_inner(znode *result, zend_ast *ast, uint32_t ty
{
CG(zend_lineno) = zend_ast_get_lineno(ast);
if (CG(memoize_mode) != ZEND_MEMOIZE_NONE) {
switch (ast->kind) {
case ZEND_AST_CALL:
case ZEND_AST_METHOD_CALL:
case ZEND_AST_NULLSAFE_METHOD_CALL:
case ZEND_AST_STATIC_CALL:
zend_compile_memoized_expr(result, ast);
return &CG(active_op_array)->opcodes[CG(active_op_array)->last - 1];
}
}
switch (ast->kind) {
case ZEND_AST_VAR:
return zend_compile_simple_var(result, ast, type, 0);