From 1b94372c9e8ee1bdba1aa96658cb9fd673005f4c Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 17 Aug 2026 22:09:58 +0000 Subject: [PATCH] Language: ArrayAccess $obj[$k] += n uses live offsetGet value (#31947) ZEND_ASSIGN_DIM_OP was TypeErroring mixed + int because the writable ArrayAccess view was used as the numeric operand. Unwrap offsetGet, then write back through offsetSet, matching ++. Co-authored-by: Cursor --- lib/VM/ArrayAccessDimension.php | 8 ++- lib/VM/Variable.php | 60 ++++++++++++++++++- phpunit.xml.dist | 2 + .../ArrayAccessAssignOp31947JITTest.php | 32 ++++++++++ .../ArrayAccessAssignOp31947VMTest.php | 30 ++++++++++ .../language/arrayaccess_assign_op_byref.phpt | 49 +++++++++++++++ 6 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 test/compliance/ArrayAccessAssignOp31947JITTest.php create mode 100644 test/compliance/ArrayAccessAssignOp31947VMTest.php create mode 100644 test/compliance/cases/language/arrayaccess_assign_op_byref.phpt diff --git a/lib/VM/ArrayAccessDimension.php b/lib/VM/ArrayAccessDimension.php index 3bc4a6e3d10..16811f113bb 100644 --- a/lib/VM/ArrayAccessDimension.php +++ b/lib/VM/ArrayAccessDimension.php @@ -24,6 +24,12 @@ public function __construct(\PHPCompiler\VM $vm, ObjectEntry $object, Variable $ $this->callerFrame = $callerFrame; } + /** + * Live offsetGet payload (Zend read_dimension BP_VAR_RW). + * + * Assign-op ($obj[$k] += n) must use this value's runtime type, not the declared mixed + * return / TYPE_ARRAYACCESS_OFFSET view (#31947, zend_vm_def.h ZEND_ASSIGN_DIM_OP). + */ public function read(): Variable { $out = new Variable(); @@ -37,7 +43,7 @@ public function read(): Variable throw new ArrayAccessOffsetSignal($catchFrame); } - return $out; + return $out->resolveIndirect(); } public function write(Variable $value): void diff --git a/lib/VM/Variable.php b/lib/VM/Variable.php index 1ff1ab31762..05b8ac98864 100755 --- a/lib/VM/Variable.php +++ b/lib/VM/Variable.php @@ -1259,6 +1259,21 @@ public function readArrayAccessOffsetValue(): self return $this->arrayAccessDimension->read()->resolveIndirect(); } + /** + * ZEND_ASSIGN_DIM_OP reads the live offsetGet payload, not the ArrayAccess view type (#31947). + * + * TYPE_ARRAYACCESS_OFFSET is named "mixed" in TypeErrors; assign-op must use the stored int/float. + */ + private static function unwrapArrayAccessOperand(self $var): self + { + $var = $var->resolveIndirect(); + if ($var->isArrayAccessOffset()) { + return $var->readArrayAccessOffsetValue(); + } + + return $var; + } + public function arrayAccessOffsetClassName(): string { if (self::TYPE_ARRAYACCESS_OFFSET !== $this->type) { @@ -2724,6 +2739,21 @@ public function bitwiseOp( return; } + if ($this->type === self::TYPE_ARRAYACCESS_OFFSET) { + $result = new self(); + $result->bitwiseOp( + $opCode, + self::unwrapArrayAccessOperand($left), + self::unwrapArrayAccessOperand($right), + $vm, + $frame + ); + $this->copyFrom($result); + + return; + } + $left = self::unwrapArrayAccessOperand($left); + $right = self::unwrapArrayAccessOperand($right); $this->reset(); restart: if ($left->type === self::TYPE_INDIRECT) { @@ -2873,8 +2903,21 @@ public function numericOp( return; } - $left = $left->resolveIndirect(); - $right = $right->resolveIndirect(); + if ($this->type === self::TYPE_ARRAYACCESS_OFFSET) { + $result = new self(); + $result->numericOp( + $opCode, + self::unwrapArrayAccessOperand($left), + self::unwrapArrayAccessOperand($right), + $vm, + $frame + ); + $this->copyFrom($result); + + return; + } + $left = self::unwrapArrayAccessOperand($left); + $right = self::unwrapArrayAccessOperand($right); TypedPropertyCheck::assertReadable($left); TypedPropertyCheck::assertReadable($right); if (OpCode::TYPE_PLUS === $opCode @@ -2981,6 +3024,19 @@ public function incDecOp( return; } + if ($this->type === self::TYPE_ARRAYACCESS_OFFSET) { + $result = new self(); + $result->incDecOp( + $opCode, + self::unwrapArrayAccessOperand($left), + self::unwrapArrayAccessOperand($right), + $vm, + $frame + ); + $this->copyFrom($result); + + return; + } if (self::TYPE_STRING_OFFSET === $this->type) { throw new \Error(self::STRING_OFFSET_INCDEC_ERROR); } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4fca96b5f89..ee006f87c75 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -245,6 +245,8 @@ ./test/compliance/ForeachByrefUninitTyped31836JITTest.php ./test/compliance/ForeachByrefUnsetRefcount31936VMTest.php ./test/compliance/ForeachByrefUnsetRefcount31936JITTest.php + ./test/compliance/ArrayAccessAssignOp31947VMTest.php + ./test/compliance/ArrayAccessAssignOp31947JITTest.php ./test/compliance/ParentPrivateStaticArrayObjectProperty31937VMTest.php ./test/compliance/VarExportArrayElementProperty31938VMTest.php ./test/compliance/VarExportArrayElementProperty31938JITTest.php diff --git a/test/compliance/ArrayAccessAssignOp31947JITTest.php b/test/compliance/ArrayAccessAssignOp31947JITTest.php new file mode 100644 index 00000000000..1c894ba1c32 --- /dev/null +++ b/test/compliance/ArrayAccessAssignOp31947JITTest.php @@ -0,0 +1,32 @@ + self::parsePHPT( + __DIR__.'/cases/language/arrayaccess_assign_op_byref.phpt', + 'arrayaccess_assign_op_byref.phpt' + ); + } + + public function setUp(): void + { + $this->BIN = realpath(__DIR__.'/../../bin/jit.php'); + } +} diff --git a/test/compliance/ArrayAccessAssignOp31947VMTest.php b/test/compliance/ArrayAccessAssignOp31947VMTest.php new file mode 100644 index 00000000000..d8927dfee7b --- /dev/null +++ b/test/compliance/ArrayAccessAssignOp31947VMTest.php @@ -0,0 +1,30 @@ + self::parsePHPT( + __DIR__.'/cases/language/arrayaccess_assign_op_byref.phpt', + 'arrayaccess_assign_op_byref.phpt' + ); + } + + public function setUp(): void + { + $this->BIN = realpath(__DIR__.'/../../bin/vm.php'); + } +} diff --git a/test/compliance/cases/language/arrayaccess_assign_op_byref.phpt b/test/compliance/cases/language/arrayaccess_assign_op_byref.phpt new file mode 100644 index 00000000000..fef9e320dda --- /dev/null +++ b/test/compliance/cases/language/arrayaccess_assign_op_byref.phpt @@ -0,0 +1,49 @@ +--TEST-- +Language: ArrayAccess by-ref offsetGet assign-op writes live int (ZEND_ASSIGN_DIM_OP; #31947) +--FILE-- + 1]; + + public function offsetExists(mixed $k): bool + { + return isset($this->d[$k]); + } + + public function &offsetGet(mixed $k): mixed + { + return $this->d[$k]; + } + + public function offsetSet(mixed $k, mixed $v): void + { + $this->d[$k] = $v; + } + + public function offsetUnset(mixed $k): void + { + unset($this->d[$k]); + } +} + +$inc = new A(); +$inc[0]++; +echo 'inc=', $inc[0], "\n"; + +$plus = new A(); +$plus[0] += 2; +echo 'plus=', $plus[0], "\n"; + +$minus = new A(); +$minus[0] -= 1; +echo 'minus=', $minus[0], "\n"; + +$mul = new A(); +$mul[0] *= 3; +echo 'mul=', $mul[0], "\n"; +--EXPECT-- +inc=2 +plus=3 +minus=0 +mul=3