fix(compiler): 按值消费引用返回调用时正确分离引用

pull/23/head
Yurun 1 month ago
parent 559a8860a0
commit 6daa312183
  1. 22
      src/CompilerBase.php
  2. 7
      src/Generator/CallArgumentGenerator.php
  3. 4
      src/Parser/ArrayExpressionTrait.php
  4. 46
      tests/compiler/ref/dynamic-return-reference-argument.phpt

@ -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;

@ -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;
}

@ -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 . ');';

@ -0,0 +1,46 @@
--TEST--
Reference-returning calls are copied by value when used as call arguments or array elements
--FILE--
<?php
function main()
{
$v1 = &test1();
var_dump($v1);
$v2 = &test1();
var_dump($v2);
var_dump($v1, $v2);
$v1 = 0;
var_dump($v1, $v2);
var_dump(test1(), test2());
var_dump([test1(), test2()]);
}
function &test1()
{
$callback = 'test2';
return $callback();
}
function &test2()
{
static $value = 0;
++$value;
return $value;
}
?>
--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)
}
Loading…
Cancel
Save