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