From 60aaced04096fa87e91a7839fd346cede3dd6107 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 10 Jul 2026 20:58:20 +0800 Subject: [PATCH] refactor(compiler): remove echo assignment error check and improve property handling - Remove unnecessary assignment expression check in echo statement parsing - Update null assignment error messages to include actual type strings - Add support for untyped properties retaining PHP mixed semantics with null values - Modify object property unset logic to preserve PHP behavior for typed properties - Implement proper parent::method() call handling with dynamic method names - Add comprehensive property inheritance compatibility checks between parent and child classes - Update documentation to reflect property visibility and inheritance requirements - Remove test case for null assignment rejection since semantics changed - Add detailed comments explaining property typing and inheritance behavior --- docs/INCOMPATIBLE_PHP_FEATURES.md | 4 +- src/CompilerBase.php | 57 ++++++++++++------- src/Translator.php | 5 ++ .../objprop-typed-object-null-unset.phpt | 11 +--- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 43ce564a..fd15e5ec 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -40,9 +40,7 @@ ## 对象模型 -- 禁止子类覆盖父类私有属性。 -- `parent::method()` 的方法名必须是字面量。 -- 通过变量持有的 clone 对象写入私有 typed property 时,可能无法完全复现 PHP 的私有属性访问语义。 +- 禁止子类用同名 `private` 属性隐藏父类私有属性;`public` / `protected` 同名声明视为同一个继承 property slot,仍须满足类型、可见性和 `readonly` 兼容性要求。 - 为避免 typed property 写入路径引入额外动态检查,native typed property 在右值类型不确定或与属性类型不一致时会退化为 `setProperty()`;部分标量赋值可能遵循 Zend 弱类型转换,而不是 AOT 默认 strict 语义。 - constructor property promotion 的运行时属性可用,但 `ReflectionProperty::isPromoted()` 目前不返回标准 PHP 结果。 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 2e554e01..edc95bfa 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1733,13 +1733,9 @@ class CompilerBase implements PropertyAccessContext { $lines = []; foreach ($v->exprs as $expr) { - if ($expr instanceof Expr\Assign) { - $this->fatalError($expr, 'Cannot echo assign expression'); - } else { - $type = $this->detectTypeOfExpr($expr); - $parsed = $this->convertExprToStringByType($this->parseExprAsValue($expr), $type); - $lines[] = 'php::echo(' . $parsed . ');'; - } + $type = $this->detectTypeOfExpr($expr); + $parsed = $this->convertExprToStringByType($this->parseExprAsValue($expr), $type); + $lines[] = 'php::echo(' . $parsed . ');'; } return implode("\n" . $this->getIndent(), $lines); @@ -5329,10 +5325,14 @@ class CompilerBase implements PropertyAccessContext $propName = $this->parseIdentifier($left->name); if ($this->isNull($right)) { - if ($this->isFixedObjectProp($def)) { + // Untyped properties retain normal PHP mixed semantics: assigning + // null is valid. Only an explicitly typed non-nullable property + // can be rejected at compile time. + if ($def->type !== self::TYPE_VAR && !$def->nullable) { + $typeStr = $this->getObjectPropertyTypeCheckTypeString($def); $this->fatalError( $left, - "Cannot assign null to {$label} `{$propName}` of fixed type `{$def->type}`" + "Cannot assign null to {$label} `{$propName}` of type `{$typeStr}`" ); } return; @@ -5560,7 +5560,11 @@ class CompilerBase implements PropertyAccessContext $propertyId = $this->getPropertyIdentifier($var, $var->var, $var->name); $def = $this->getNativePropertyDef($var); if ($def) { - if ($this->isFixedObjectProp($def)) { + // Object typed properties are backed by Zend object + // properties, so PHP can represent their uninitialized + // state after unset(). Keep that behavior instead of + // restoring a fixed default value. + if ($this->isFixedObjectProp($def) && $def->type !== self::TYPE_OBJECT) { $restoreDefault = $this->getFixedObjectPropDefaultValue($def); if ($restoreDefault === null) { $this->fatalError($var, "Cannot unset object property `{$this->parseIdentifier($var->name)}` of fixed type `{$def->type}` without default value"); @@ -6833,6 +6837,13 @@ class CompilerBase implements PropertyAccessContext $callScope = []; $class = $this->parseIdentifier($expr->class); + // parent::$method() still has a lexical parent class even when the + // method name itself is dynamic. Handle it before the generic dynamic + // static-call branch below. + if ($this->isNameExpr($expr->class) && $class === 'parent') { + return $this->parseParentMethodCall($expr); + } + if ($this->isVarExpr($expr->class) or $this->isVarExpr($expr->name)) { $var = $class; if ($this->isTypedObject($var)) { @@ -6857,8 +6868,6 @@ class CompilerBase implements PropertyAccessContext if ($class === 'self') { $class = $this->class; $self = true; - } elseif ($class === 'parent') { - return $this->parseParentMethodCall($expr); } elseif ($class === 'std') { return $this->parseStdCall($expr); } @@ -8119,21 +8128,25 @@ class CompilerBase implements PropertyAccessContext protected function parseParentMethodCall(Expr\StaticCall $expr): string { - $methodStr = $this->classDef->name . '::' . $this->parseIdentifier($expr->name); if (!$this->classDef->extends) { - $this->fatalError($expr, 'Cannot call parent method `' . $methodStr . '()` because class `' . $this->classDef->name . '` does not extend any class'); - } - if (!$this->isIdExpr($expr->name)) { - $this->fatalError($expr, 'Cannot call parent method `' . $methodStr . '()` because method name is not a literal'); + $this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class'); } $parentClass = $this->classDef->extends; - $method = $this->parseIdentifier($expr->name); - $this->guardAbstractMethod($parentClass, $method, $expr); - // TODO 是否转为 native 调用 + if ($this->isIdExpr($expr->name)) { + $method = $this->parseIdentifier($expr->name); + $this->guardAbstractMethod($parentClass, $method, $expr); + $methodPtr = $this->getMethodPtr($parentClass, $method); + } else { + // parent:: is bound to the lexical parent class, not the runtime + // object's parent. Look the method up on that class, then invoke it + // through this_ so Zend receives the current call scope. + $methodPtr = 'php::getMethod(' . $this->getClassEntryPtr($parentClass) . ', ' + . $this->identifierToStr($expr->name) . ')'; + } if (empty($expr->args)) { - return 'this_.call(' . $this->getMethodPtr($parentClass, $method) . ')'; + return 'this_.call(' . $methodPtr . ')'; } - return 'this_.call(' . $this->getMethodPtr($parentClass, $method) . ', ' . $this->parseCallArgs($expr->args) . ')'; + return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args) . ')'; } protected function genDebugInfo(?NodeAbstract $stmt = null, string $functionName = '', int $startLine = 0): string diff --git a/src/Translator.php b/src/Translator.php index f2b29a8b..50a7e5ad 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -4095,6 +4095,11 @@ CODE; foreach ($this->classDef->properties as $name => $childProp) { if ($chainNode->hasProperty($name)) { $parentProp = $chainNode->getProperty($name); + // A parent private property would be a separate PHP slot + // hidden by the child declaration. TypePHP forbids that + // dual-slot model. Public/protected declarations instead + // describe the same inherited property slot and must obey + // PHP-compatible type, visibility and readonly rules. if ($parentProp->flags & Modifiers::PRIVATE) { $this->fatalError($classStmt, "Declaration of `{$className}::\${$name}` conflicts with private property " . diff --git a/tests/aot/optimizations/objprop-typed-object-null-unset.phpt b/tests/aot/optimizations/objprop-typed-object-null-unset.phpt index 76fb6c85..9dd18aaa 100644 --- a/tests/aot/optimizations/objprop-typed-object-null-unset.phpt +++ b/tests/aot/optimizations/objprop-typed-object-null-unset.phpt @@ -1,5 +1,5 @@ --TEST-- -SSA object prop: typed object property rejects null and supports unset +SSA object prop: typed object property supports unset --FILE-- prop)); var_dump($this->prop->name()); - try { - $this->prop = null; - } catch (TypeError $e) { - var_dump($e->getMessage()); - } - var_dump(isset($this->prop)); - $this->prop = new ObjPropValue(); unset($this->prop); var_dump(isset($this->prop)); @@ -54,8 +47,6 @@ function main(): void { --EXPECT-- bool(true) string(5) "value" -string(61) "ObjPropHolder::$prop must be of type ObjPropValue, null given" -bool(true) bool(false) string(5) "value" string(5) "value"