From a4820ff7594205e2f1fba48e1cacfaeb88bf2cef Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 2 Sep 2026 08:36:28 +0800 Subject: [PATCH] fix(coalesce): preserve reference array containers --- src/Parser/AssignOpTrait.php | 22 +++- .../assign-coalesce-reference-container.phpt | 103 ++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/coalesce/assign-coalesce-reference-container.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 9e9fe3a9..645fb4d4 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -1795,7 +1795,11 @@ trait AssignOpTrait ) { $target->var = $this->stabilizeCoalesceTarget($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)) { $target->dim = $this->materializeCoalesceTargetSubexpr($target->dim, false); @@ -1813,7 +1817,11 @@ trait AssignOpTrait || $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); $this->appendCapturedStmtLinesToContext($before); @@ -1826,6 +1834,16 @@ trait AssignOpTrait $this->addLocalVar($tmp, $this->getNativeObjectPointerType($class)); $this->addNativeObject($tmp, $class); $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 { // Keep the value in a Variant: the rewritten target then goes // through the generic Zend handlers, which also covers receivers diff --git a/tests/compiler/coalesce/assign-coalesce-reference-container.phpt b/tests/compiler/coalesce/assign-coalesce-reference-container.phpt new file mode 100644 index 00000000..ba7c9da6 --- /dev/null +++ b/tests/compiler/coalesce/assign-coalesce-reference-container.phpt @@ -0,0 +1,103 @@ +--TEST-- +??= preserves references returned as writable array containers +--FILE-- +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) +}