From 8b2c5decc26646c3f03f31fac8383c76d3f8882d Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 12 Jul 2026 19:58:27 +0800 Subject: [PATCH] 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 --- src/Entity/PropertyDef.php | 6 ++ src/Optimizer/SsaPropOptimizer.php | 19 +++- src/Parser/PropertyAccessTrait.php | 4 +- src/Preprocessor.php | 1 + .../readonly-cross-class-direct.phpt | 95 +++++++++++++++++++ 5 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 tests/aot/object_property/readonly-cross-class-direct.phpt diff --git a/src/Entity/PropertyDef.php b/src/Entity/PropertyDef.php index 157bed61..14c7ce03 100644 --- a/src/Entity/PropertyDef.php +++ b/src/Entity/PropertyDef.php @@ -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); diff --git a/src/Optimizer/SsaPropOptimizer.php b/src/Optimizer/SsaPropOptimizer.php index 53de7828..0e7a5155 100644 --- a/src/Optimizer/SsaPropOptimizer.php +++ b/src/Optimizer/SsaPropOptimizer.php @@ -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]); } diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 94165d92..11036c10 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -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; } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index ab08a9b2..0601e3e2 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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; diff --git a/tests/aot/object_property/readonly-cross-class-direct.phpt b/tests/aot/object_property/readonly-cross-class-direct.phpt new file mode 100644 index 00000000..c6d95245 --- /dev/null +++ b/tests/aot/object_property/readonly-cross-class-direct.phpt @@ -0,0 +1,95 @@ +--TEST-- +Readonly native properties remain initialized when read across classes +--FILE-- +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)