diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 1217d171..79c93bdd 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -501,6 +501,9 @@ class CompilerBase extends \PhpAot\Core\Translator public function getObjectType(string $object): string { + if (isset($this->context->stableObjects[$object])) { + return $this->context->stableObjects[$object]; + } return $this->context->objects[$object] ?? 'stdClass'; } diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index 342157a6..e2b5a119 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -8,13 +8,8 @@ namespace PhpAot\Php\Context; -use PhpAot\Php\Analysis\SsaBuilder; - class FunctionContext { - /** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ - public ?SsaBuilder $ssaBuilder = null; - /** Map of SSA-stable object variable name => class name (SsaPropOptimizer). */ public array $stableObjects = []; @@ -105,4 +100,19 @@ class FunctionContext $this->scopeLevel--; unset($this->scopeLayouts[$this->scopeLevel]); } + + public function resetAnalysisTemporaries(array $localVars, int $tmpVarIndex): void + { + $this->localVars = $localVars; + $this->tmpVarIndex = $tmpVarIndex; + $this->beforeStmtLines = []; + $this->afterStmtLines = []; + $this->objectProps = []; + $this->hoistedProps = []; + $this->staticPropRefs = []; + $this->scopeLayouts = []; + $this->scopeLevel = 0; + $this->inLoop = false; + $this->inAssignExpr = false; + } } diff --git a/src/Php/Optimizer/LoopVarOptimizer.php b/src/Php/Optimizer/LoopVarOptimizer.php index fdbcac17..08f504e3 100644 --- a/src/Php/Optimizer/LoopVarOptimizer.php +++ b/src/Php/Optimizer/LoopVarOptimizer.php @@ -10,6 +10,7 @@ namespace PhpAot\Php\Optimizer; +use PhpAot\Php\Analysis\SsaBuilder; use PhpAot\Php\Analysis\SsaFlags; use PhpParser\Node; use PhpParser\Node\Expr; @@ -27,13 +28,8 @@ trait LoopVarOptimizer 'mb_substr_count' => true, ]; - protected function optimizeLoopVars(): void + protected function optimizeLoopVars(SsaBuilder $ssa): void { - $ssa = $this->context->ssaBuilder; - if (!$ssa) { - return; - } - $stmts = $ssa->getStmts(); if (!$stmts) { return; @@ -49,7 +45,7 @@ trait LoopVarOptimizer || $this->isSuperGlobal($escapedName)) { continue; } - if (!$this->isLoopSsaVarStable($varName)) { + if (!$this->isLoopSsaVarStable($ssa, $varName)) { continue; } if ($this->loopVarHasUnsafeUsage($varName, $stmts, $candidate['allowed'] ?? [])) { @@ -57,7 +53,7 @@ trait LoopVarOptimizer } $depFailed = false; foreach ($candidate['deps'] ?? [] as $depName => $_) { - if (!$this->isLoopSsaVarStable($depName) + if (!$this->isLoopSsaVarStable($ssa, $depName) || $this->loopVarHasUnsafeUsage($depName, $stmts, $candidates[$depName]['allowed'] ?? [])) { $depFailed = true; break; @@ -578,13 +574,8 @@ trait LoopVarOptimizer return false; } - protected function isLoopSsaVarStable(string $varName): bool + protected function isLoopSsaVarStable(SsaBuilder $ssa, string $varName): bool { - $ssa = $this->context->ssaBuilder; - if (!$ssa) { - return false; - } - foreach ($ssa->ssaVars as $ssaVar) { if ($ssaVar->origName !== $varName) { continue; diff --git a/src/Php/Optimizer/SsaPropOptimizer.php b/src/Php/Optimizer/SsaPropOptimizer.php index 25dc1fda..a23ad5d5 100644 --- a/src/Php/Optimizer/SsaPropOptimizer.php +++ b/src/Php/Optimizer/SsaPropOptimizer.php @@ -41,10 +41,9 @@ trait SsaPropOptimizer * This must be done during analysis because $this->context->objects is only * populated during code generation (after analysis). */ - protected function optimizeObjectProps(): void + protected function optimizeObjectProps(SsaBuilder $ssa): void { - $ssa = $this->context->ssaBuilder; - if (!$ssa || !$this->nativeTypes) { + if (!$this->nativeTypes) { return; } @@ -63,15 +62,17 @@ trait SsaPropOptimizer return; } - $objectAssigns = $this->collectObjectAssignments($ssa->getStmts()); - - // Also check function parameters that are typed objects + // Seed with function parameters that are typed objects, then propagate + // simple object aliases such as `$next = $right`. + $objectAssigns = []; foreach ($this->context->objects as $objName => $className) { if ($objName === 'this_') { continue; } $objectAssigns[$objName] = $className; } + $objectAliases = []; + $objectAssigns = $this->collectObjectAssignments($ssa->getStmts(), $objectAssigns, $objectAliases); foreach ($objectAssigns as $objName => $className) { if ($objName === 'this_') { @@ -82,7 +83,7 @@ trait SsaPropOptimizer continue; } - if (!$this->isObjectSsaStable($ssa, $objName)) { + if (!$this->isObjectSsaStable($ssa, $objName, $objectAssigns)) { continue; } @@ -90,7 +91,10 @@ trait SsaPropOptimizer continue; } - $unsafeProps = $this->collectDangerousPropOps($objName, $ssa->getStmts()); + $unsafeProps = $this->collectDangerousPropOpsForObjects( + $this->getObjectAliasNames($objName, $objectAliases), + $ssa->getStmts() + ); if ($unsafeProps) { $this->context->unsafeObjectProps[$objName] = $unsafeProps; } @@ -103,57 +107,102 @@ trait SsaPropOptimizer * Walk the function body AST to find variable assignments that produce * typed objects. Returns map of varName => className. */ - protected function collectObjectAssignments(array $stmts): array + protected function collectObjectAssignments(array $stmts, array $knownObjects = [], array &$aliases = []): array { - $result = []; - foreach ($stmts as $stmt) { - $this->scanStmtForObjectAssign($stmt, $result); - } + $result = $knownObjects; + do { + $count = count($result); + foreach ($stmts as $stmt) { + $this->scanStmtForObjectAssign($stmt, $result, $aliases); + } + } while (count($result) !== $count); + return $result; } - protected function scanStmtForObjectAssign($stmt, array &$result): void + protected function scanStmtForObjectAssign($stmt, array &$result, array &$aliases): void { if (!$stmt instanceof Node) { return; } - if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Assign) { - $assign = $stmt->expr; - $var = $assign->var; - if ($var instanceof Expr\Variable && is_string($var->name)) { - $className = $this->resolveNewExprClass($assign->expr); - if ($className) { - $result[$var->name] = $className; - } - } + if ($stmt instanceof Node\Stmt\Expression) { + $this->scanExprForObjectAssign($stmt->expr, $result, $aliases); return; } // Recurse if ($stmt instanceof Node\Stmt\If_) { - foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); foreach ($stmt->elseifs as $elseif) { - foreach ($elseif->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($elseif->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); } if ($stmt->else) { - foreach ($stmt->else->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($stmt->else->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); } } elseif ($stmt instanceof Node\Stmt\While_ || $stmt instanceof Node\Stmt\Do_) { - foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); } elseif ($stmt instanceof Node\Stmt\For_ || $stmt instanceof Node\Stmt\Foreach_) { - foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); } elseif ($stmt instanceof Node\Stmt\TryCatch) { - foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); foreach ($stmt->catches as $catch) { - foreach ($catch->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($catch->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); } if ($stmt->finally) { - foreach ($stmt->finally->stmts as $s) $this->scanStmtForObjectAssign($s, $result); + foreach ($stmt->finally->stmts as $s) $this->scanStmtForObjectAssign($s, $result, $aliases); } } } + protected function scanExprForObjectAssign($expr, array &$result, array &$aliases): void + { + if (!$expr instanceof Node) { + return; + } + + if ($expr instanceof Expr\Assign) { + if ($expr->expr instanceof Node) { + $this->scanExprForObjectAssign($expr->expr, $result, $aliases); + } + + $assign = $expr; + $var = $assign->var; + if ($var instanceof Expr\Variable && is_string($var->name)) { + $className = $this->resolveAssignedObjectClass($assign->expr, $result); + if ($className) { + $result[$var->name] = $className; + if ($assign->expr instanceof Expr\Variable && is_string($assign->expr->name)) { + $aliases[$var->name] = $assign->expr->name; + } + } + } + return; + } + + foreach ($expr->getSubNodeNames() as $subNodeName) { + $subNode = $expr->$subNodeName; + if ($subNode instanceof Node) { + $this->scanExprForObjectAssign($subNode, $result, $aliases); + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if ($item instanceof Node) { + $this->scanExprForObjectAssign($item, $result, $aliases); + } + } + } + } + } + + protected function resolveAssignedObjectClass(Expr $expr, array $knownObjects): ?string + { + if ($expr instanceof Expr\Variable && is_string($expr->name)) { + return $knownObjects[$expr->name] ?? null; + } + + return $this->resolveNewExprClass($expr); + } + /** * Resolve the class name from a `new ClassName()` expression, * or a function/method call that returns a known object type. @@ -188,7 +237,7 @@ trait SsaPropOptimizer /** * Check if an object variable has a single stable SSA definition. */ - protected function isObjectSsaStable(SsaBuilder $ssa, string $objName): bool + protected function isObjectSsaStable(SsaBuilder $ssa, string $objName, array $knownObjects = []): bool { $foundDef = false; @@ -209,7 +258,7 @@ trait SsaPropOptimizer return false; // Multiple definitions } - if (!$this->isObjectDefinition($ssaVar)) { + if (!$this->isObjectDefinition($ssaVar, $knownObjects)) { return false; } @@ -223,7 +272,7 @@ trait SsaPropOptimizer * Check if an SSA definition sets the variable to an object value. * Accepts both `new ClassName()` and calls that return a typed object. */ - protected function isObjectDefinition($ssaVar): bool + protected function isObjectDefinition($ssaVar, array $knownObjects = []): bool { $def = $ssaVar->definition; if (!$def) { @@ -232,6 +281,9 @@ trait SsaPropOptimizer if ($def instanceof Node\Stmt\Expression && $def->expr instanceof Expr\Assign) { $rhs = $def->expr->expr; + if ($rhs instanceof Expr\Variable && is_string($rhs->name)) { + return isset($knownObjects[$rhs->name]); + } if ($rhs instanceof Expr\New_) { return true; } @@ -245,6 +297,26 @@ trait SsaPropOptimizer return false; } + protected function getObjectAliasNames(string $objName, array $aliases): array + { + $names = [$objName => true]; + $changed = true; + while ($changed) { + $changed = false; + foreach ($aliases as $alias => $source) { + if (isset($names[$alias]) && !isset($names[$source])) { + $names[$source] = true; + $changed = true; + } + if (isset($names[$source]) && !isset($names[$alias])) { + $names[$alias] = true; + $changed = true; + } + } + } + return array_keys($names); + } + /** * Check if a class has no magic methods that intercept property access. */ @@ -283,10 +355,19 @@ trait SsaPropOptimizer * @return array property name map; '*' means any property may be invalidated. */ protected function collectDangerousPropOps(string $objName, array $stmts): array + { + return $this->collectDangerousPropOpsForObjects([$objName], $stmts); + } + + /** + * @param string[] $objNames aliases that may point at the same object + * @return array property name map; '*' means any property may be invalidated. + */ + protected function collectDangerousPropOpsForObjects(array $objNames, array $stmts): array { $events = []; foreach ($stmts as $stmt) { - $this->collectPropEvents($stmt, $objName, $events); + $this->collectPropEvents($stmt, $objNames, $events); } return $this->unsafePropsFromEvents($events); } @@ -301,7 +382,7 @@ trait SsaPropOptimizer /** * @param array $events */ - protected function collectPropEvents($node, string $objName, array &$events): void + protected function collectPropEvents($node, string|array $objName, array &$events): void { if (!$node instanceof Node) { return; @@ -382,7 +463,7 @@ trait SsaPropOptimizer } if (!$this->isSafeObjectExposureCall($node)) { if (($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) - && $this->isVarNamed($node->var, $objName)) { + && $this->isVarNamedAny($node->var, $objName)) { $events[] = ['kind' => 'danger', 'prop' => '*']; } foreach ($node->args as $arg) { @@ -421,7 +502,7 @@ trait SsaPropOptimizer if ($node instanceof Expr\Closure) { foreach ($node->uses as $use) { - if ($this->isVarNamed($use->var, $objName)) { + if ($this->isVarNamedAny($use->var, $objName)) { $events[] = ['kind' => 'danger', 'prop' => '*']; } } @@ -506,7 +587,7 @@ trait SsaPropOptimizer * * @param array $events */ - protected function collectPropEventsInDynamicParts($node, string $objName, array &$events): void + protected function collectPropEventsInDynamicParts($node, string|array $objName, array &$events): void { if (!$node instanceof Expr\PropertyFetch) { return; @@ -561,17 +642,17 @@ trait SsaPropOptimizer /** * Check if an expression is a property fetch on a specific object. */ - protected function isPropOfObj($node, string $objName): bool + protected function isPropOfObj($node, string|array $objName): bool { return $this->getPropNameOfObj($node, $objName) !== null; } - protected function getPropNameOfObj($node, string $objName): ?string + protected function getPropNameOfObj($node, string|array $objName): ?string { if (!$node instanceof Expr\PropertyFetch || !$node->var instanceof Expr\Variable || !is_string($node->var->name) - || !$this->isVarNamed($node->var, $objName)) { + || !$this->isVarNamedAny($node->var, $objName)) { return null; } @@ -582,20 +663,30 @@ trait SsaPropOptimizer return '*'; } - protected function exprMayExposeObject($node, string $objName): bool + protected function isVarNamedAny($node, string|array $varNames): bool + { + foreach ((array)$varNames as $varName) { + if ($this->isVarNamed($node, $varName)) { + return true; + } + } + return false; + } + + protected function exprMayExposeObject($node, string|array $objName): bool { if (!$node instanceof Node) { return false; } - if ($this->isVarNamed($node, $objName)) { + if ($this->isVarNamedAny($node, $objName)) { return true; } if ($node instanceof Expr\PropertyFetch && $node->var instanceof Expr\Variable && is_string($node->var->name) - && $this->isVarNamed($node->var, $objName)) { + && $this->isVarNamedAny($node->var, $objName)) { return false; } @@ -622,12 +713,12 @@ trait SsaPropOptimizer return false; } - protected function isDynamicPropWriteOfObj($node, string $objName): bool + protected function isDynamicPropWriteOfObj($node, string|array $objName): bool { if ($node instanceof Expr\PropertyFetch && $node->var instanceof Expr\Variable && is_string($node->var->name) - && $this->isVarNamed($node->var, $objName)) { + && $this->isVarNamedAny($node->var, $objName)) { return $this->getPropNameOfObj($node, $objName) === '*'; } diff --git a/src/Php/Optimizer/SsaTypeOptimizer.php b/src/Php/Optimizer/SsaTypeOptimizer.php index 42950f7d..74ed3e41 100644 --- a/src/Php/Optimizer/SsaTypeOptimizer.php +++ b/src/Php/Optimizer/SsaTypeOptimizer.php @@ -71,10 +71,9 @@ trait SsaTypeOptimizer * killed, or defined by a φ function with mixed sources, pre-set the * type in localVars so genScopeVarDecl emits the narrow C++ type. */ - protected function optimizeVarTypes(): void + protected function optimizeVarTypes(SsaBuilder $ssa): void { - $ssa = $this->context->ssaBuilder; - if (!$ssa || empty($ssa->ssaVars)) { + if (empty($ssa->ssaVars)) { return; } @@ -93,6 +92,18 @@ trait SsaTypeOptimizer $groups[$name][] = $ssaVar; } + // Type detection may inspect RHS expressions that read variables + // defined by earlier assignments. Code generation normally registers + // those locals while parsing assignments, but SSA optimization runs + // before that parse pass, so seed the optimization context with the + // SSA-defined local names as generic Vars. + foreach (array_keys($groups) as $name) { + $varName = $this->escapeVarName($name); + if (!isset($this->context->arguments[$varName]) && !$this->hasVar($varName)) { + $this->context->localVars[$varName] = self::TYPE_VAR; + } + } + foreach ($groups as $varName => $varList) { $varName = $this->escapeVarName($varName); // Skip parameters — they already have declared types @@ -166,7 +177,7 @@ trait SsaTypeOptimizer } // Scan for operations that SSA definition types alone can't detect - $functionStmts = $this->context->ssaBuilder->getStmts(); + $functionStmts = $ssa->getStmts(); if ($functionStmts) { if ($narrowedType === self::TYPE_INT && $this->hasDangerousIntOps($varName, $functionStmts)) { continue; diff --git a/src/Php/Parser/TypeDetectionTrait.php b/src/Php/Parser/TypeDetectionTrait.php index 8005dc41..715b8ce3 100644 --- a/src/Php/Parser/TypeDetectionTrait.php +++ b/src/Php/Parser/TypeDetectionTrait.php @@ -17,7 +17,7 @@ trait TypeDetectionTrait { public function isTypedObject(string $object): bool { - return isset($this->context->objects[$object]); + return isset($this->context->objects[$object]) || isset($this->context->stableObjects[$object]); } protected function isSuperGlobal(string $var): bool diff --git a/src/Php/Translator.php b/src/Php/Translator.php index d8494388..1cafc04b 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -1930,8 +1930,8 @@ CODE; // 读取 link-paths $linkPaths = $cfg['link-paths'] ?? null; if (!empty($linkPaths) && is_array($linkPaths)) { - foreach ($linkPaths as $path) { - $this->linkPaths[] = (string)$path; + foreach ($linkPaths as $linkPath) { + $this->linkPaths[] = (string)$linkPath; } } @@ -2781,14 +2781,18 @@ CODE; // Build SSA/e-SSA analysis for this function if ($v->stmts) { - $this->context->ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList); - $this->context->ssaBuilder->build(); + $oriLocalVars = $this->context->localVars; + $oriTmpVarIndex = $this->context->tmpVarIndex; + /** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ + $ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList); + $ssaBuilder->build(); // Narrow local variable types based on SSA analysis - $this->optimizeVarTypes(); + $this->optimizeVarTypes($ssaBuilder); // Narrow range-proven loop counters independent of native_types - $this->optimizeLoopVars(); + $this->optimizeLoopVars($ssaBuilder); // Analyze object stability for property reference hoisting - $this->optimizeObjectProps(); + $this->optimizeObjectProps($ssaBuilder); + $this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex); } $stmts = ''; @@ -2863,6 +2867,10 @@ CODE; */ protected function checkParentMethodCanBeOverridden(Node\Stmt\ClassMethod $v, string $name): void { + if ($name === '__construct') { + return; + } + $classDef = $this->classDef; $childFuncDef = $this->methodDef->functionDef; while (true) { diff --git a/tests/aot/concat_flatten.phpt b/tests/aot/concat_flatten.phpt new file mode 100644 index 00000000..e9083988 --- /dev/null +++ b/tests/aot/concat_flatten.phpt @@ -0,0 +1,22 @@ +--TEST-- +Concat flattening to variadic php::concat(ArgList) +--FILE-- + +--EXPECT-- +string(12) "hello world!" +string(13) "pre_42_suffix" +string(2) "ab" diff --git a/tests/aot/optimizations/objprop-hoist-object-alias-escape.phpt b/tests/aot/optimizations/objprop-hoist-object-alias-escape.phpt new file mode 100644 index 00000000..114512b1 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-object-alias-escape.phpt @@ -0,0 +1,31 @@ +--TEST-- +SSA object prop: object alias escape prevents property hoisting +--FILE-- +a; + $ref = 99; +} + +function run(Foo $right): void { + $next = $right; + $next->a = 1; + + make_ref($right); + $next->a += 1; + + var_dump($next->a); +} + +function main(): void { + run(new Foo()); +} +?> +--EXPECT-- +int(100)