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
pull/17/head
韩天峰 2 months ago
parent df268d19f4
commit 60aaced040
  1. 4
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 57
      src/CompilerBase.php
  3. 5
      src/Translator.php
  4. 11
      tests/aot/optimizations/objprop-typed-object-null-unset.phpt

@ -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 结果。

@ -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

@ -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 " .

@ -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--
<?php
use native_types;
@ -28,13 +28,6 @@ class ObjPropHolder {
var_dump(isset($this->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"

Loading…
Cancel
Save