refactor(php): 重构SSA分析和对象属性优化实现

- 移除FunctionContext中的ssaBuilder属性,改为函数内部临时创建
- 修改optimizeLoopVars、optimizeObjectProps、optimizeVarTypes方法接受SsaBuilder参数
- 实现对象别名传播检测以改进属性提升优化
- 添加resetAnalysisTemporaries方法重置分析临时数据
- 更新变量类型检测逻辑以支持SSA定义的局部变量
- 修复构造函数可以被重写的检查逻辑
- 扩展isTypedObject方法以包含稳定对象检测
- 为字符串连接扁平化和对象属性逃逸分析添加测试用例
pull/3/head
韩天峰 2 months ago
parent 545f347d4a
commit 2800b974e3
  1. 3
      src/Php/CompilerBase.php
  2. 20
      src/Php/Context/FunctionContext.php
  3. 19
      src/Php/Optimizer/LoopVarOptimizer.php
  4. 185
      src/Php/Optimizer/SsaPropOptimizer.php
  5. 19
      src/Php/Optimizer/SsaTypeOptimizer.php
  6. 2
      src/Php/Parser/TypeDetectionTrait.php
  7. 22
      src/Php/Translator.php
  8. 22
      tests/aot/concat_flatten.phpt
  9. 31
      tests/aot/optimizations/objprop-hoist-object-alias-escape.phpt

@ -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';
}

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

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

@ -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<string, bool> 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<string, bool> 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<int, array{kind: string, prop: string}> $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<int, array{kind: string, prop: string}> $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) === '*';
}

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

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

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

@ -0,0 +1,22 @@
--TEST--
Concat flattening to variadic php::concat(ArgList)
--FILE--
<?php
function main(): void {
$a = 'hello';
$b = ' ';
$c = 'world';
$d = '!';
var_dump($a . $b . $c . $d);
$x = 'pre_';
$y = 42;
var_dump($x . $y . '_suffix');
var_dump('a' . 'b');
}
?>
--EXPECT--
string(12) "hello world!"
string(13) "pre_42_suffix"
string(2) "ab"

@ -0,0 +1,31 @@
--TEST--
SSA object prop: object alias escape prevents property hoisting
--FILE--
<?php
use native_types;
class Foo {
public int $a;
}
function make_ref(Foo $o): void {
$ref =& $o->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)
Loading…
Cancel
Save