Skip to content
Open
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
9 changes: 6 additions & 3 deletions Zend/Optimizer/compact_literals.c
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,18 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
case ZEND_POST_INC_OBJ:
case ZEND_POST_DEC_OBJ:
if (opline->op2_type == IS_CONST) {
uint32_t obj_flags = (opline->opcode == ZEND_ASSIGN_OBJ || opline->opcode == ZEND_ASSIGN_OBJ_REF)
? ZEND_ASSIGN_OBJ_FLAGS
: ZEND_FETCH_OBJ_FLAGS;
// op2 property
if (opline->op1_type == IS_UNUSED &&
property_slot[opline->op2.constant] >= 0) {
opline->extended_value = property_slot[opline->op2.constant] | (opline->extended_value & ZEND_FETCH_OBJ_FLAGS);
opline->extended_value = property_slot[opline->op2.constant] | (opline->extended_value & obj_flags);
} else {
opline->extended_value = cache_size | (opline->extended_value & ZEND_FETCH_OBJ_FLAGS);
opline->extended_value = cache_size | (opline->extended_value & obj_flags);
cache_size += 3 * sizeof(void *);
if (opline->op1_type == IS_UNUSED) {
property_slot[opline->op2.constant] = opline->extended_value & ~ZEND_FETCH_OBJ_FLAGS;
property_slot[opline->op2.constant] = opline->extended_value & ~obj_flags;
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_after_parent.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
Promoted readonly property reassignment - child can reassign after parent::__construct()
--FILE--
<?php

// When both parent and child have CPP for the same property, the child's
// reassignment window must survive the parent constructor's exit cleanup.

class P {
public function __construct(
public readonly string $x = 'P_default',
) {
// Parent does NOT reassign
}
}

class C extends P {
public function __construct(
public readonly string $x = 'C_default',
) {
try {
parent::__construct();
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
// Child should still be able to reassign its own CPP property
$this->x = 'C_reassigned';
}
}

$c = new C();
var_dump($c->x);

// Also test with multiple instances
$c2 = new C();
var_dump($c2->x);

?>
--EXPECT--
Error: Cannot modify readonly property C::$x
string(12) "C_reassigned"
Error: Cannot modify readonly property C::$x
string(12) "C_reassigned"
28 changes: 28 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Promoted readonly property reassignment in constructor - basic
--FILE--
<?php

class Point {
public function __construct(
public readonly int $x = 0,
public readonly int $y = 0,
) {
// Reassign promoted readonly properties - allowed once
$this->x = abs($x);
$this->y = abs($y);
}
}

$point = new Point();
var_dump($point->x, $point->y);

$point2 = new Point(-5, -3);
var_dump($point2->x, $point2->y);

?>
--EXPECT--
int(0)
int(0)
int(5)
int(3)
89 changes: 89 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_child_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
--TEST--
Promoted readonly property reassignment in constructor - child cannot reassign parent's property
--FILE--
<?php

// Case 1: Parent does NOT use reassignment, child still cannot reassign
class Parent1 {
public function __construct(
public readonly string $prop = 'parent default',
) {
// Parent does NOT reassign here
}
}

class Child1 extends Parent1 {
public function __construct() {
parent::__construct();
// Child cannot reassign parent-owned promoted property
try {
$this->prop = 'child override';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$child = new Child1();
var_dump($child->prop);

// Case 2: Parent USES reassignment, child cannot
class Parent2 {
public function __construct(
public readonly string $prop = 'parent default',
) {
$this->prop = 'parent set'; // Uses the one reassignment
}
}

class Child2 extends Parent2 {
public function __construct() {
parent::__construct();
// Child cannot reassign parent-owned promoted property
try {
$this->prop = 'child override';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$child2 = new Child2();
var_dump($child2->prop);

// Case 3: Child with its own promoted property
class Parent3 {
public function __construct(
public readonly string $parentProp = 'parent default',
) {
// Parent does NOT reassign here
}
}

class Child3 extends Parent3 {
public function __construct(
public readonly string $childProp = 'child default',
) {
parent::__construct();
// Child cannot reassign parent's property, but can reassign its own
try {
$this->parentProp = 'child set parent';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
$this->childProp = 'child set own';
}
}

$child3 = new Child3();
var_dump($child3->parentProp, $child3->childProp);

?>
--EXPECT--
Error: Cannot modify readonly property Parent1::$prop
string(14) "parent default"
Error: Cannot modify readonly property Parent2::$prop
string(10) "parent set"
Error: Cannot modify readonly property Parent3::$parentProp
string(14) "parent default"
string(13) "child set own"
31 changes: 31 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_child_preempt_parent.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Promoted readonly property reassignment in constructor - child preempt then parent ctor throws
--FILE--
<?php

class ParentCPP {
public function __construct(
public readonly string $prop = 'parent default',
) {
$this->prop = 'parent set';
}
}

class ChildCPP extends ParentCPP {
public function __construct() {
$this->prop = 'child set';
try {
parent::__construct();
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$c = new ChildCPP();
var_dump($c->prop);

?>
--EXPECT--
Error: Cannot modify readonly property ParentCPP::$prop
string(9) "child set"
84 changes: 84 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_child_redefine.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
--TEST--
Promoted readonly property reassignment in constructor - child redefines parent property
--FILE--
<?php

// Case 1: Parent uses CPP, child redefines as non-promoted, child tries to reassign.
// P1 owns the CPP reassignment window; it is cleared when P1's constructor exits,
// before C1's body runs. So C1's write attempt fails.
class P1 {
public function __construct(
public readonly string $x = 'P',
) {}
}

class C1 extends P1 {
public readonly string $x;

public function __construct() {
parent::__construct();
try {
$this->x = 'C';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$c1 = new C1();
var_dump($c1->x);

// Case 2: Parent uses CPP and reassigns; child redefines as non-promoted.
// The child does not use CPP, so it does not claim CPP ownership of the property.
// P2's CPP "owns" the reassignment window: P2's body write succeeds.
class P2 {
public function __construct(
public readonly string $x = 'P1',
) {
$this->x = 'P2';
}
}

class C2 extends P2 {
public readonly string $x;

public function __construct() {
parent::__construct();
}
}

$c2 = new C2();
var_dump($c2->x);

// Case 3: Parent uses CPP, child uses CPP redefinition.
// Child's CPP opens the reassignment window for C3::$x. When parent::__construct()
// runs, P3's CPP tries to initialize C3::$x again, which must fail since C3
// owns the property and has already initialized it.
class P3 {
public function __construct(
public readonly string $x = 'P',
) {}
}

class C3 extends P3 {
public function __construct(
public readonly string $x = 'C1',
) {
try {
parent::__construct();
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$c3 = new C3();
var_dump($c3->x);

?>
--EXPECT--
Error: Cannot modify readonly property C1::$x
string(1) "P"
string(2) "P2"
Error: Cannot modify readonly property C3::$x
string(2) "C1"
23 changes: 23 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_conditional.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Promoted readonly property reassignment in constructor - conditional initialization
--FILE--
<?php

class Config {
public function __construct(
public readonly ?string $cacheDir = null,
) {
$this->cacheDir ??= '/tmp/app_cache';
}
}

$config1 = new Config();
var_dump($config1->cacheDir);

$config2 = new Config('/custom/cache');
var_dump($config2->cacheDir);

?>
--EXPECT--
string(14) "/tmp/app_cache"
string(13) "/custom/cache"
34 changes: 34 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_different_object.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
Promoted readonly property reassignment in constructor - different object fails
--FILE--
<?php

// Constructor can modify its own promoted property, but not another object's
class Foo {
public function __construct(
public readonly int $x = 0,
?Foo $other = null,
) {
$this->x = $x * 2;
if ($other !== null) {
try {
$other->x = 999;
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}
}

$a = new Foo(5);
var_dump($a->x);

$b = new Foo(3, $a);
var_dump($a->x, $b->x); // $a unchanged

?>
--EXPECT--
int(10)
Error: Cannot modify readonly property Foo::$x
int(10)
int(6)
29 changes: 29 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_direct_ctor_call.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
Promoted readonly properties cannot be reassigned when __construct() is called directly
--FILE--
<?php

class Foo {
public function __construct(
public readonly string $value = 'default',
) {
$this->value = strtoupper($this->value);
}
}

$obj = new Foo('hello');
var_dump($obj->value);

// Direct call fails: CPP assignment cannot reinitialize an already-set property
try {
$obj->__construct('world');
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
var_dump($obj->value);

?>
--EXPECT--
string(5) "HELLO"
Error: Cannot modify readonly property Foo::$value
string(5) "HELLO"
Loading
Loading