From c7d4b562c0ddc69bcdc82854e711b56879ba91a4 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:39:25 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(parser):=20=E4=BF=AE=E5=A4=8D=E6=95=B0?= =?UTF-8?q?=E7=BB=84=E5=85=83=E7=B4=A0=E5=BC=95=E7=94=A8=E8=B5=8B=E5=80=BC?= =?UTF-8?q?=E5=86=99=E5=9B=9E=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Parser/AssignOpTrait.php | 17 +++++++++- tests/compiler/ref/array-ref-assign-001.phpt | 29 +++++++++++++++++ tests/compiler/ref/array-ref-assign-002.phpt | 34 ++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/compiler/ref/array-ref-assign-001.phpt create mode 100644 tests/compiler/ref/array-ref-assign-002.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index dabef684..5fdb32b4 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -42,11 +42,24 @@ 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; + 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) { + return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')'; + } return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')'; } @@ -828,7 +841,9 @@ trait AssignOpTrait $left = $this->parseIdentifier($expr->var); $rightExpr = $tmpVar . ' = ' . $this->emitStaticPropertyFetchRef($expr->expr, $expr); } elseif ($this->isArrayDimFetch($expr->expr)) { - $left = $this->parseIdentifier($expr->var); + // $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'); diff --git a/tests/compiler/ref/array-ref-assign-001.phpt b/tests/compiler/ref/array-ref-assign-001.phpt new file mode 100644 index 00000000..3273f29e --- /dev/null +++ b/tests/compiler/ref/array-ref-assign-001.phpt @@ -0,0 +1,29 @@ +--TEST-- +array reference assignment: append and element assignment write back through reference +--FILE-- + +--EXPECT-- +array(3) { + [0]=> + &int(123) + [1]=> + &int(456) + [2]=> + int(3) +} +array(2) { + [0]=> + &int(123) + [1]=> + &int(456) +} diff --git a/tests/compiler/ref/array-ref-assign-002.phpt b/tests/compiler/ref/array-ref-assign-002.phpt new file mode 100644 index 00000000..025e6bad --- /dev/null +++ b/tests/compiler/ref/array-ref-assign-002.phpt @@ -0,0 +1,34 @@ +--TEST-- +array reference assignment to element: $arr[$k] = &$v writes back through reference +--FILE-- + +--EXPECT-- +int(100) +int(200) +int(111) +int(222) +int(77) From a43828446c1a7c2c30ad643ade9341fc5b8e181e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 17:42:29 +0800 Subject: [PATCH 2/2] fix(parser): preserve references across all array write paths --- src/Parser/AssignOpTrait.php | 31 ++++++++-------- tests/compiler/ref/array-ref-assign-002.phpt | 17 +++++++++ tests/compiler/ref/array-ref-assign-003.phpt | 37 ++++++++++++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 tests/compiler/ref/array-ref-assign-003.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 5fdb32b4..745f9be2 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -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({$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'); diff --git a/tests/compiler/ref/array-ref-assign-002.phpt b/tests/compiler/ref/array-ref-assign-002.phpt index 025e6bad..ac441ca0 100644 --- a/tests/compiler/ref/array-ref-assign-002.phpt +++ b/tests/compiler/ref/array-ref-assign-002.phpt @@ -2,6 +2,12 @@ array reference assignment to element: $arr[$k] = &$v writes back through reference --FILE-- 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) diff --git a/tests/compiler/ref/array-ref-assign-003.phpt b/tests/compiler/ref/array-ref-assign-003.phpt new file mode 100644 index 00000000..6e9f8ca4 --- /dev/null +++ b/tests/compiler/ref/array-ref-assign-003.phpt @@ -0,0 +1,37 @@ +--TEST-- +dynamically typed array element assignment preserves references and ArrayAccess writes +--FILE-- + +--EXPECT-- +int(123) +int(123) +int(234) +int(234) +int(456)