Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/VM/ArrayAccessDimension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -37,7 +43,7 @@ public function read(): Variable
throw new ArrayAccessOffsetSignal($catchFrame);
}

return $out;
return $out->resolveIndirect();
}

public function write(Variable $value): void
Expand Down
60 changes: 58 additions & 2 deletions lib/VM/Variable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@
<file>./test/compliance/ForeachByrefUninitTyped31836JITTest.php</file>
<file>./test/compliance/ForeachByrefUnsetRefcount31936VMTest.php</file>
<file>./test/compliance/ForeachByrefUnsetRefcount31936JITTest.php</file>
<file>./test/compliance/ArrayAccessAssignOp31947VMTest.php</file>
<file>./test/compliance/ArrayAccessAssignOp31947JITTest.php</file>
<file>./test/compliance/ParentPrivateStaticArrayObjectProperty31937VMTest.php</file>
<file>./test/compliance/VarExportArrayElementProperty31938VMTest.php</file>
<file>./test/compliance/VarExportArrayElementProperty31938JITTest.php</file>
Expand Down
32 changes: 32 additions & 0 deletions test/compliance/ArrayAccessAssignOp31947JITTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

require_once __DIR__.'/../BaseTest.php';

/**
* JIT: ArrayAccess by-ref offsetGet assign-op uses the live int (#31947).
*
* Dedicated provider — path-slash data-set names break --filter on full JITTest.
*
* @group llvm
*/
final class ArrayAccessAssignOp31947JITTest extends BaseTest
{
protected static string $DIR = __DIR__;

public static function providePHPTests(): \Generator
{
yield 'arrayaccess_assign_op_byref.phpt' => 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');
}
}
30 changes: 30 additions & 0 deletions test/compliance/ArrayAccessAssignOp31947VMTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

require_once __DIR__.'/../BaseTest.php';

/**
* VM: ArrayAccess by-ref offsetGet assign-op uses the live int (#31947).
*
* Dedicated provider — path-slash data-set names break --filter on full VMTest.
*/
final class ArrayAccessAssignOp31947VMTest extends BaseTest
{
protected static string $DIR = __DIR__;

public static function providePHPTests(): \Generator
{
yield 'arrayaccess_assign_op_byref.phpt' => 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');
}
}
49 changes: 49 additions & 0 deletions test/compliance/cases/language/arrayaccess_assign_op_byref.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
--TEST--
Language: ArrayAccess by-ref offsetGet assign-op writes live int (ZEND_ASSIGN_DIM_OP; #31947)
--FILE--
<?php
class A implements ArrayAccess
{
private $d = [0 => 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
Loading