From 6daa3121834cf183c859f11755ab1866e7e1c52f Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 15 Jul 2026 21:17:52 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(compiler):=20=E6=8C=89=E5=80=BC?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E5=BC=95=E7=94=A8=E8=BF=94=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=97=B6=E6=AD=A3=E7=A1=AE=E5=88=86=E7=A6=BB=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CompilerBase.php | 22 +++++++++ src/Generator/CallArgumentGenerator.php | 7 +++ src/Parser/ArrayExpressionTrait.php | 4 +- .../dynamic-return-reference-argument.phpt | 46 +++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/ref/dynamic-return-reference-argument.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 074a1517..5888454c 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1066,6 +1066,28 @@ class CompilerBase implements PropertyAccessContext return $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr)); } + /** + * 把一个"按引用返回"的调用在按值消费处解引用为值快照。 + * + * 返回引用的调用会产生一个指向被调用方存储的活引用。当该调用被按值消费 + * (例如作为按值函数参数、数组元素、按值返回等会触发 PHP 分离语义的上下文) + * 时,PHP 会在求值那一刻拷贝出值的快照,因此之后对别名存储的修改不应再可见。 + * 我们通过把结果赋值给一个临时 php::Var 来分离引用(普通的 Var 赋值会断开 + * 引用,而 php::Variant(php::Ref) 构造会保留引用),从而保留从左到右的求值顺序。 + * + * 注意:二元/一元运算等操作数上下文应保持引用活动、在运算时读值,不应在此快照; + * 那些上下文由各自的解析器直接保留引用。 + */ + protected function materializeRefReturnAsValue(NodeAbstract $value, string $expr): string + { + if ($value instanceof Expr\CallLike && $this->resolveRefReturningCall($value) !== false) { + $tmpVar = $this->addTmpVar(Type::VAR); + $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $expr . ';'; + return $tmpVar; + } + return $expr; + } + protected function getObjectPropVarName(string $object, string $prop): string { return self::OBJECT_PROP . $object . self::NAMESPACE_SEPARATOR . $prop; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 0347667e..79431f24 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -586,6 +586,13 @@ trait CallArgumentGenerator protected function materializeCallArgValue(NodeAbstract $value, string $expr): string { + // A call that returns by reference yields a live php::Ref aliasing the + // callee's storage. When such a call feeds a by-value argument, PHP takes + // a value snapshot at evaluation time (left to right), so later mutations + // to the aliased storage must not be observable. The dynamic ArgList keeps + // references verbatim (Ctor::CopyRef), so we dereference into a temporary + // value at the point of the call. + $expr = $this->materializeRefReturnAsValue($value, $expr); if (!$this->shouldMaterializeCallArg($value)) { return $expr; } diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index a920f62b..819cf8cf 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -60,7 +60,7 @@ trait ArrayExpressionTrait $this->indentLevel++; foreach ($items as $item) { $this->assertExprCanBeUsedAsValue($item->value, 'array value'); - $value = $this->parseIdentifier($item->value); + $value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value)); if ($item->key) { $this->assertExprCanBeUsedAsValue($item->key, 'array key'); $key = $this->parseArrayKey($item->key); @@ -221,7 +221,7 @@ trait ArrayExpressionTrait } $value = $this->convertToRef($item->value); } else { - $value = $this->parseIdentifier($item->value); + $value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value)); } if ($item->unpack) { $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');'; diff --git a/tests/compiler/ref/dynamic-return-reference-argument.phpt b/tests/compiler/ref/dynamic-return-reference-argument.phpt new file mode 100644 index 00000000..471881ef --- /dev/null +++ b/tests/compiler/ref/dynamic-return-reference-argument.phpt @@ -0,0 +1,46 @@ +--TEST-- +Reference-returning calls are copied by value when used as call arguments or array elements +--FILE-- + +--EXPECT-- +int(1) +int(2) +int(2) +int(2) +int(0) +int(0) +int(1) +int(2) +array(2) { + [0]=> + int(3) + [1]=> + int(4) +} From 1128fb06c822daa7e0a1d24ad8556e35a36bb949 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 19 Jul 2026 11:15:56 +0800 Subject: [PATCH 2/2] fix(compiler): preserve ref snapshot evaluation order --- src/CompilerBase.php | 17 +++------- .../dynamic-return-reference-argument.phpt | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 5888454c..e92ffb80 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1067,23 +1067,16 @@ class CompilerBase implements PropertyAccessContext } /** - * 把一个"按引用返回"的调用在按值消费处解引用为值快照。 - * - * 返回引用的调用会产生一个指向被调用方存储的活引用。当该调用被按值消费 - * (例如作为按值函数参数、数组元素、按值返回等会触发 PHP 分离语义的上下文) - * 时,PHP 会在求值那一刻拷贝出值的快照,因此之后对别名存储的修改不应再可见。 - * 我们通过把结果赋值给一个临时 php::Var 来分离引用(普通的 Var 赋值会断开 - * 引用,而 php::Variant(php::Ref) 构造会保留引用),从而保留从左到右的求值顺序。 - * - * 注意:二元/一元运算等操作数上下文应保持引用活动、在运算时读值,不应在此快照; - * 那些上下文由各自的解析器直接保留引用。 + * Snapshot a reference-returning call before a by-value container can retain + * its php::Ref. Assigning to an existing Var detaches the reference, unlike + * constructing a Variant directly from Ref. Keep the assignment inline so + * earlier arguments or array elements retain PHP's evaluation order. */ protected function materializeRefReturnAsValue(NodeAbstract $value, string $expr): string { if ($value instanceof Expr\CallLike && $this->resolveRefReturningCall($value) !== false) { $tmpVar = $this->addTmpVar(Type::VAR); - $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $expr . ';'; - return $tmpVar; + return '(' . $tmpVar . ' = ' . $expr . ')'; } return $expr; } diff --git a/tests/compiler/ref/dynamic-return-reference-argument.phpt b/tests/compiler/ref/dynamic-return-reference-argument.phpt index 471881ef..aede4d2a 100644 --- a/tests/compiler/ref/dynamic-return-reference-argument.phpt +++ b/tests/compiler/ref/dynamic-return-reference-argument.phpt @@ -14,6 +14,9 @@ function main() var_dump($v1, $v2); var_dump(test1(), test2()); var_dump([test1(), test2()]); + var_dump(['first' => test1(), test2()]); + var_dump(value_order('arg-left'), ref_order('arg-ref')); + var_dump([value_order('array-left'), ref_order('array-ref')]); } function &test1() @@ -28,6 +31,19 @@ function &test2() ++$value; return $value; } + +function value_order(string $label): string +{ + echo "$label\n"; + return $label; +} + +function &ref_order(string $label) +{ + static $value = 42; + echo "$label\n"; + return $value; +} ?> --EXPECT-- int(1) @@ -44,3 +60,21 @@ array(2) { [1]=> int(4) } +array(2) { + ["first"]=> + int(5) + [0]=> + int(6) +} +arg-left +arg-ref +string(8) "arg-left" +int(42) +array-left +array-ref +array(2) { + [0]=> + string(10) "array-left" + [1]=> + int(42) +}