diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index acdb85fd..0aff8465 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -2067,6 +2067,19 @@ class CompilerBase extends \PhpAot\Core\Translator } } break; + case 'Expr_StaticPropertyFetch': + if ($this->isIdExpr($expr->name)) { + if (!$expr->hasAttribute('nativePropertyDef')) { + $class = null; + $this->findNativeStaticProperty($expr, $class); + } + if ($expr->hasAttribute('nativePropertyDef')) { + /** @var PropertyDef $def */ + $def = $expr->getAttribute('nativePropertyDef'); + return $def->type; + } + } + break; case 'Expr_ArrayDimFetch': if ($this->isStdArrayExpr($expr)) { if (!$expr->hasAttribute('stdArrayDimFetch')) { @@ -3604,6 +3617,57 @@ class CompilerBase extends \PhpAot\Core\Translator } } + protected function assertCanAssignStaticProp(Expr\StaticPropertyFetch $left, Expr $right): void + { + if (!$left->hasAttribute('nativePropertyDef')) { + return; + } + + /** @var PropertyDef $def */ + $def = $left->getAttribute('nativePropertyDef'); + $propName = $this->parseIdentifier($left->name); + + if ($this->isNull($right)) { + if ($this->isFixedObjectProp($def)) { + $this->fatalError( + $left, + "Cannot assign null to static property `{$propName}` of fixed type `{$def->type}`" + ); + } + return; + } + + if ($def->type !== self::TYPE_OBJECT) { + return; + } + + $rightType = $this->detectTypeOfExpr($right); + if ($rightType !== self::TYPE_VAR && $rightType !== self::TYPE_OBJECT) { + $this->fatalError( + $left, + "Cannot assign value of type `{$rightType}` to static property `{$propName}` of type `{$def->type}`" + ); + } + + if ($def->class === '') { + return; + } + + $rightClass = $this->detectClassOfExpr($right); + if ($rightClass === '') { + $this->fatalError( + $left, + "Cannot assign object of unknown class to static property `{$propName}` of class `{$def->class}`" + ); + } + if ($rightClass !== $def->class) { + $this->fatalError( + $left, + "Cannot assign object of class `{$rightClass}` to static property `{$propName}` of class `{$def->class}`" + ); + } + } + protected function parseUnset(Node\Stmt\Unset_ $node): string { $vars = $node->vars; @@ -4591,24 +4655,22 @@ class CompilerBase extends \PhpAot\Core\Translator $class = null; $nativeProp = $this->findNativeStaticProperty($expr, $class); if ($nativeProp) { - // Hoist typed int/float static properties to C++ native references, - // analogous to the $this->intProp reference optimization. if ($this->nativeTypes && $expr->hasAttribute('nativePropertyDef')) { /** @var PropertyDef $def */ $def = $expr->getAttribute('nativePropertyDef'); - if ($def->type === self::TYPE_INT || $def->type === self::TYPE_FLOAT) { - $propName = $this->parseIdentifier($expr->name); - $refVar = '_static_' . str_replace('\\', '_', $class) . '_' . $propName; - if (!isset($this->context->staticPropRefs[$refVar])) { - $classPtr = $this->getClassEntryPtr($class); - $this->context->staticPropRefs[$refVar] = [ - 'type' => $def->type, - 'classPtr' => $classPtr, - 'offsetExpr' => $nativeProp, - ]; - } - return $refVar; + $propName = $this->parseIdentifier($expr->name); + $refVar = '_static_' . str_replace('\\', '_', $class) . '_' . $propName; + if (!isset($this->context->staticPropRefs[$refVar])) { + $info = $this->getHoistedObjectPropInfo($def->type); + $classPtr = $this->getClassEntryPtr($class); + $this->context->staticPropRefs[$refVar] = [ + 'type' => $info['type'], + 'classPtr' => $classPtr, + 'offsetExpr' => $nativeProp, + 'kind' => $info['kind'], + ]; } + return $refVar; } if ($expr->hasAttribute('nativeProperty')) { @@ -5317,8 +5379,12 @@ class CompilerBase extends \PhpAot\Core\Translator } foreach ($this->context->staticPropRefs as $name => $info) { $getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')'; - $zvalMacro = ($info['type'] === self::TYPE_FLOAT) ? 'Z_DVAL_P' : 'Z_LVAL_P'; - $code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = ' . $zvalMacro . '(' . $getter . '.unwrap_ptr());' . PHP_EOL; + if (($info['kind'] ?? 'zval') === 'var') { + $code .= $this->getIndent() . self::TYPE_VAR . ' ' . $name . ' = ' . $getter . ';' . PHP_EOL; + } else { + $zvalMacro = ($info['type'] === self::TYPE_FLOAT) ? 'Z_DVAL_P' : 'Z_LVAL_P'; + $code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = ' . $zvalMacro . '(' . $getter . '.unwrap_ptr());' . PHP_EOL; + } } return $code; } diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index 7be2630a..d1413ff3 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -58,7 +58,7 @@ class FunctionContext public array $beforeStmtLines = []; public array $afterStmtLines = []; public array $objectProps; - /** Map of ref var name => ['type' => ..., 'class' => ..., 'prop' => ..., 'ceExpr' => ..., 'offsetExpr' => ...] */ + /** Map of static property local slots. int/float use zval refs; other types use Var slots. */ public array $staticPropRefs = []; public int $scopeLevel = 0; /** diff --git a/src/Php/Optimizer/SsaPropOptimizer.php b/src/Php/Optimizer/SsaPropOptimizer.php index e733ede1..8935f1ba 100644 --- a/src/Php/Optimizer/SsaPropOptimizer.php +++ b/src/Php/Optimizer/SsaPropOptimizer.php @@ -45,6 +45,17 @@ trait SsaPropOptimizer return; } + if ($this->classDef && !$this->classDef->trait) { + if ($this->isClassSafeForPropHoisting($this->getFullClassName())) { + $unsafeProps = $this->collectDangerousPropOps('this_', $ssa->getStmts()); + if ($unsafeProps) { + $this->context->unsafeObjectProps['this_'] = $unsafeProps; + } + } else { + $this->context->unsafeObjectProps['this_'] = ['*' => true]; + } + } + if (empty($ssa->ssaVars)) { return; } diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index b3aae194..9db7f9f7 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -263,6 +263,8 @@ trait AssignOpTrait if ($this->isPropertyFetch($left)) { $this->assertCanAssignObjectProp($left, $right); + } elseif ($this->isStaticPropertyFetch($left)) { + $this->assertCanAssignStaticProp($left, $right); } $rightExpr = $this->parseAssignRightExpr($right); diff --git a/tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt b/tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt new file mode 100644 index 00000000..d5f89318 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt @@ -0,0 +1,31 @@ +--TEST-- +SSA object prop: this object argument escape prevents property hoisting +--FILE-- +a = 1; + + $fn = 'make_ref'; + $fn($this); + $this->a += 1; + + var_dump($this->a); + } +} + +function make_ref(Foo $o): void { + $ref =& $o->a; + $ref = 99; +} + +function main(): void { + (new Foo())->run(); +} +?> +--EXPECT-- +int(100) diff --git a/tests/aot/static/static-prop-native-defaults.phpt b/tests/aot/static/static-prop-native-defaults.phpt new file mode 100644 index 00000000..e5f8c404 --- /dev/null +++ b/tests/aot/static/static-prop-native-defaults.phpt @@ -0,0 +1,53 @@ +--TEST-- +Static properties with native_types keep defaults and local slots +--FILE-- + +--EXPECT-- +int(42) +float(3.5) +bool(true) +string(4) "seed" +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} +int(100) +float(2.25) +bool(false) +string(7) "changed" +array(1) { + [0]=> + int(9) +}