fix(coalesce): preserve reference array containers

master
韩天峰 14 hours ago
parent 456f1a38b2
commit a4820ff759
  1. 22
      src/Parser/AssignOpTrait.php
  2. 103
      tests/compiler/coalesce/assign-coalesce-reference-container.phpt

@ -1795,7 +1795,11 @@ trait AssignOpTrait
) { ) {
$target->var = $this->stabilizeCoalesceTarget($target->var); $target->var = $this->stabilizeCoalesceTarget($target->var);
} elseif (!$this->isCoalesceTargetTrivialSubexpr($target->var)) { } elseif (!$this->isCoalesceTargetTrivialSubexpr($target->var)) {
$target->var = $this->materializeCoalesceTargetSubexpr($target->var, false); $target->var = $this->materializeCoalesceTargetSubexpr(
$target->var,
isReceiver: false,
writableContainer: true,
);
} }
if ($target->dim !== null && !$this->isCoalesceTargetTrivialSubexpr($target->dim)) { if ($target->dim !== null && !$this->isCoalesceTargetTrivialSubexpr($target->dim)) {
$target->dim = $this->materializeCoalesceTargetSubexpr($target->dim, false); $target->dim = $this->materializeCoalesceTargetSubexpr($target->dim, false);
@ -1813,7 +1817,11 @@ trait AssignOpTrait
|| $expr instanceof Expr\ClassConstFetch; || $expr instanceof Expr\ClassConstFetch;
} }
private function materializeCoalesceTargetSubexpr(Expr $sub, bool $isReceiver): Expr\Variable private function materializeCoalesceTargetSubexpr(
Expr $sub,
bool $isReceiver,
bool $writableContainer = false,
): Expr\Variable
{ {
[$code, $before, $after] = $this->parseExprWithCapturedStmts($sub); [$code, $before, $after] = $this->parseExprWithCapturedStmts($sub);
$this->appendCapturedStmtLinesToContext($before); $this->appendCapturedStmtLinesToContext($before);
@ -1826,6 +1834,16 @@ trait AssignOpTrait
$this->addLocalVar($tmp, $this->getNativeObjectPointerType($class)); $this->addLocalVar($tmp, $this->getNativeObjectPointerType($class));
$this->addNativeObject($tmp, $class); $this->addNativeObject($tmp, $class);
$cleanup = $tmp . ' = nullptr;'; $cleanup = $tmp . ' = nullptr;';
} elseif ($writableContainer && $this->resolveRefReturningCall($sub) !== false) {
// A call result used as an array write target may be a reference to
// external storage. Boxing it in a Variant applies normal PHP value
// semantics and dereferences it, so the later write would modify a
// detached array copy. A Reference temporary preserves known
// by-reference returns as well as runtime-resolved dynamic calls;
// assigning a normal value to it still provides the disposable
// temporary container required by PHP.
$tmp = $this->addTmpVar(Type::REF);
$cleanup = $tmp . '.unset();';
} else { } else {
// Keep the value in a Variant: the rewritten target then goes // Keep the value in a Variant: the rewritten target then goes
// through the generic Zend handlers, which also covers receivers // through the generic Zend handlers, which also covers receivers

@ -0,0 +1,103 @@
--TEST--
??= preserves references returned as writable array containers
--FILE--
<?php
function keyName(string $label): string
{
echo "key:$label\n";
return 'value';
}
function rhs(string $label): int
{
echo "rhs:$label\n";
return 42;
}
function &referencedValues(): array
{
global $values;
return $values;
}
function valuesByValue(): array
{
return [];
}
class ReferenceContainer
{
private array $values = [];
public function &values(): array
{
return $this->values;
}
public function all(): array
{
return $this->values;
}
}
function main(): void
{
global $values;
// A known by-reference function must write through to the global array.
$values = [];
referencedValues()[keyName('function-unset')] ??= rhs('function-unset');
var_dump($values);
// The set branch still evaluates the key once and keeps the RHS lazy.
$values = ['value' => 7];
var_dump(referencedValues()[keyName('function-set')] ??= rhs('function-set'));
var_dump($values);
// A dynamic call is resolved only at runtime, so its reference identity
// must survive the same container stabilization boundary.
$callback = 'referencedValues';
$values = [];
$callback()[keyName('dynamic')] ??= rhs('dynamic');
var_dump($values);
// The conservative dynamic-call path must also accept an ordinary
// by-value array result as a disposable write target.
$callback = 'valuesByValue';
var_dump($callback()[keyName('dynamic-value')] ??= rhs('dynamic-value'));
// Method return references follow the same write-through rules.
$container = new ReferenceContainer();
$container->values()[keyName('method')] ??= rhs('method');
var_dump($container->all());
}
?>
--EXPECT--
key:function-unset
rhs:function-unset
array(1) {
["value"]=>
int(42)
}
key:function-set
int(7)
array(1) {
["value"]=>
int(7)
}
key:dynamic
rhs:dynamic
array(1) {
["value"]=>
int(42)
}
key:dynamic-value
rhs:dynamic-value
int(42)
key:method
rhs:method
array(1) {
["value"]=>
int(42)
}
Loading…
Cancel
Save