fix(parser): preserve references across all array write paths

pull/33/head
韩天峰 1 month ago
parent 77fc5920b6
commit a43828446c
  1. 31
      src/Parser/AssignOpTrait.php
  2. 17
      tests/compiler/ref/array-ref-assign-002.phpt
  3. 37
      tests/compiler/ref/array-ref-assign-003.phpt

@ -42,24 +42,27 @@ trait AssignOpTrait
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, Type::VAR);
// 仅当目标是 php::Array 时使用 item/newItem:
// - item(dim, true) 直接返回元素 zval 地址,赋值时能穿透 IS_REFERENCE 写回,
// 修复 $arr = [&$x] / $arr[] = &$x 这类数组元素引用的写回问题;
// - 对于 ArrayAccess 对象(如 ArrayObject)或类型未知(VAR)的变量,item 不存在或语义不符,
// 必须继续使用 offsetSet(对象数组元素的引用写回由对象自身保证,编译器不负责)。
$isPhpArray = $this->getVarType($array) === Type::ARRAY;
// item(dim, true) updates an existing reference's value, while offsetSet()
// replaces the array bucket and breaks the reference. Keep offsetSet() for
// ArrayAccess objects; dynamically typed/reference containers need a
// runtime array check because either representation is possible.
$arrayType = $this->getVarType($array);
if ($left->dim === null) {
if ($isPhpArray) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.newItem() = {$tmp}" . '), ' . $tmp . ')';
}
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')';
}
$dim = $this->parseIdentifier($left->dim);
if ($isPhpArray) {
if ($arrayType === Type::ARRAY) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')';
}
if ($arrayType === Type::VAR || $arrayType === Type::REF) {
$writeArray = "static_cast<void>({$array}.item({$dim}, true) = {$tmp})";
$writeOther = "{$array}.offsetSet({$dim}, {$tmp})";
return $code . '((' . $tmp . ' = ' . $value . ', '
. "({$array}.isArray() ? {$writeArray} : {$writeOther})"
. '), ' . $tmp . ')';
}
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')';
}
@ -759,6 +762,9 @@ trait AssignOpTrait
}
$left = $this->parseWritableIdentifier($expr->var);
// Keep this write-context form for every RHS kind. Re-parsing it as a
// read later breaks append and missing-key targets such as
// `$array[] =& $source`.
if ($this->isVarExpr($expr->var)) {
if (!$this->hasVar($left)) {
@ -835,15 +841,10 @@ trait AssignOpTrait
}
}
} elseif ($this->isPropertyFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr);
} elseif ($this->isStaticPropertyFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$rightExpr = $tmpVar . ' = ' . $this->emitStaticPropertyFetchRef($expr->expr, $expr);
} elseif ($this->isArrayDimFetch($expr->expr)) {
// $left 已在函数开头通过 parseWritableIdentifier($expr->var) 正确计算,
// 这里不可再用 parseIdentifier() 覆盖,否则当左值是数组追加($arr[] = &$x)
// 或数组元素($arr[$k] = &$x)时会被当作读取而报错 "Cannot use [] for reading"。
$array = $this->parseWritableIdentifier($expr->expr->var);
if ($expr->expr->dim == null) {
$this->fatalError($expr, 'Cannot assign reference to array dim fetch without dim');

@ -2,6 +2,12 @@
array reference assignment to element: $arr[$k] = &$v writes back through reference
--FILE--
<?php
class RefSource
{
public $value = 30;
public static $staticValue = 40;
}
function main()
{
$x = 10;
@ -24,6 +30,15 @@ function main()
$m[0][0] = &$z;
$z = 77;
var_dump($m[0][0]); // 77
// 左侧数组追加/元素写入不可因右侧是属性引用而被重新按读取解析
$source = new RefSource();
$propertyRefs = [];
$propertyRefs[] = &$source->value;
$propertyRefs[2] = &RefSource::$staticValue;
$propertyRefs[0] = 333;
$propertyRefs[2] = 444;
var_dump($source->value, RefSource::$staticValue);
}
?>
--EXPECT--
@ -32,3 +47,5 @@ int(200)
int(111)
int(222)
int(77)
int(333)
int(444)

@ -0,0 +1,37 @@
--TEST--
dynamically typed array element assignment preserves references and ArrayAccess writes
--FILE--
<?php
function writeElement(mixed $container, mixed $key, mixed $value): void
{
$container[$key] = $value;
}
function writeReferencedContainer(mixed &$container, mixed $value): void
{
$container[0] = $value;
}
function main()
{
$referenced = 10;
$array = [&$referenced];
writeElement($array, 0, 123);
var_dump($referenced, $array[0]);
$referencedAgain = 20;
$arrayByReference = [&$referencedAgain];
writeReferencedContainer($arrayByReference, 234);
var_dump($referencedAgain, $arrayByReference[0]);
$object = new ArrayObject();
writeElement($object, 'key', 456);
var_dump($object['key']);
}
?>
--EXPECT--
int(123)
int(123)
int(234)
int(234)
int(456)
Loading…
Cancel
Save