feat(parser): add readonly property support with hoisting restrictions

- Add readonly flag to PropertyDef entity
- Implement readonly check in property hoisting logic
- Prevent hoisting of readonly properties in SsaPropOptimizer
- Update canHoist methods to accept PropertyDef parameter
- Add test case for readonly cross-class property access
- Document readonly property restriction in hoisting rules
pull/17/head
韩天峰 1 month ago
parent 30ebc309da
commit 8b2c5decc2
  1. 6
      src/Entity/PropertyDef.php
  2. 19
      src/Optimizer/SsaPropOptimizer.php
  3. 4
      src/Parser/PropertyAccessTrait.php
  4. 1
      src/Preprocessor.php
  5. 95
      tests/aot/object_property/readonly-cross-class-direct.phpt

@ -22,6 +22,7 @@ class PropertyDef
public array $typeCheck = [];
public string $typeStr = '';
public bool $promoted = false;
public bool $readonly = false;
public ?string $getter = null;
public ?string $setter = null;
@ -54,6 +55,11 @@ class PropertyDef
return $this->flags & Modifiers::STATIC;
}
public function isReadonly(): bool
{
return $this->readonly;
}
public function isPrivateSet(): bool
{
return (bool) ($this->flags & Modifiers::PRIVATE_SET);

@ -17,6 +17,7 @@
* 6. No func(&$o->prop) (property passed by reference)
* 7. Object is not exposed to dynamic user code before later property access
* 8. First access is not inside a loop or nested block scope
* 9. Property is not readonly (including properties of a readonly class)
*
* Direct unset($o->prop) is not dangerous: the object handlers reset/reject the
* unset path, so a hoisted reference is not invalidated by direct unset alone.
@ -24,6 +25,7 @@
namespace TypePhp\Optimizer;
use TypePhp\Entity\PropertyDef;
use TypePhp\Type;
use TypePhp\Analysis\SsaBuilder;
@ -754,24 +756,31 @@ trait SsaPropOptimizer
return isset($this->context->stableObjects[$objName]);
}
public function canHoistStableObjectProp(string $objName, string $propName): bool
public function canHoistStableObjectProp(string $objName, string $propName, PropertyDef $property): bool
{
if (!$this->isStableObject($objName)) {
return false;
}
return $this->canHoistObjectPropBySafety($objName, $propName);
return $this->canHoistObjectPropBySafety($objName, $propName, $property);
}
public function canHoistObjectProp(string $objName, string $propName): bool
public function canHoistObjectProp(string $objName, string $propName, PropertyDef $property): bool
{
if ($objName !== 'this_' && !$this->isStableObject($objName)) {
return false;
}
return $this->canHoistObjectPropBySafety($objName, $propName);
return $this->canHoistObjectPropBySafety($objName, $propName, $property);
}
protected function canHoistObjectPropBySafety(string $objName, string $propName): bool
protected function canHoistObjectPropBySafety(
string $objName,
string $propName,
PropertyDef $property,
): bool
{
if ($property->isReadonly()) {
return false;
}
$unsafeProps = $this->context->unsafeObjectProps[$objName] ?? [];
return !isset($unsafeProps['*']) && !isset($unsafeProps[$propName]);
}

@ -898,7 +898,7 @@ trait PropertyAccessTrait
$propVar = $this->getObjectPropVarName($objectVar, $propName);
if ($objectVar === 'this_') {
if (!$this->canHoistObjectProp($objectVar, $propName)) {
if (!$this->canHoistObjectProp($objectVar, $propName, $def)) {
return null;
}
$this->registerHoistedObjectPropVar($propVar, $def->type, $getter);
@ -907,7 +907,7 @@ trait PropertyAccessTrait
return $propVar;
}
if (!$this->canHoistStableObjectProp($objectVar, $propName)) {
if (!$this->canHoistStableObjectProp($objectVar, $propName, $def)) {
return null;
}

@ -710,6 +710,7 @@ class Preprocessor extends CompilerBase
}
$propDef = new PropertyDef($name, $flags, $type, $default, $nullable);
$propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY);
$propDef->class = $class;
$propDef->arrayInitPlan = $arrayInitPlan;
$propDef->promoted = $promoted;

@ -0,0 +1,95 @@
--TEST--
Readonly native properties remain initialized when read across classes
--FILE--
<?php
use native_types;
class ReadonlyDimensions
{
public readonly int $x;
public readonly int $y;
public readonly float $scale;
public function __construct(int $x, int $y, float $scale)
{
$this->x = $x;
$this->y = $y;
$this->scale = $scale;
}
public function values(): array
{
return [$this->x, $this->y, $this->scale];
}
}
class MutableDimensions
{
public int $x;
public function __construct(int $x)
{
$this->x = $x;
}
}
readonly class ReadonlyClassDimensions
{
public int $x;
public function __construct(int $x)
{
$this->x = $x;
}
}
class DimensionsReader
{
public static function readonlyValues(ReadonlyDimensions $value): array
{
return [$value->x, $value->y, $value->scale];
}
public static function mutableValue(MutableDimensions $value): int
{
return $value->x;
}
public static function readonlyClassValue(ReadonlyClassDimensions $value): int
{
return $value->x;
}
}
function main(): void
{
$readonly = new ReadonlyDimensions(10, 20, 1.5);
$mutable = new MutableDimensions(30);
$readonlyClass = new ReadonlyClassDimensions(40);
var_dump(DimensionsReader::readonlyValues($readonly));
var_dump($readonly->values());
var_dump(DimensionsReader::mutableValue($mutable));
var_dump(DimensionsReader::readonlyClassValue($readonlyClass));
}
?>
--EXPECT--
array(3) {
[0]=>
int(10)
[1]=>
int(20)
[2]=>
float(1.5)
}
array(3) {
[0]=>
int(10)
[1]=>
int(20)
[2]=>
float(1.5)
}
int(30)
int(40)
Loading…
Cancel
Save