- 在CompilerBase中集成LoopVarOptimizer trait - 实现range-proven循环变量优化算法,支持while和for循环 - 添加多项式时间复杂度的循环计数器类型窄化功能 - 集成安全检查机制防止不安全的整数运算推导 - 添加针对strlen、count等函数的非负整数类型识别 - 实现循环边界安全检测避免溢出风险 - 新增多种循环模式的测试用例验证优化效果 - 更新现有测试注释以反映新的类型推断行为pull/1/head
parent
cdd0b05bf7
commit
dbaa5a10e0
20 changed files with 1736 additions and 93 deletions
@ -0,0 +1,734 @@ |
||||
<?php |
||||
/** |
||||
* Range-proven loop variable optimizer. |
||||
* |
||||
* This pass narrows common PHP loop counters to php::Int without requiring |
||||
* `use native_types`. It is intentionally pattern-based: PHP arithmetic can |
||||
* widen integers to floats on overflow, so only monotonic counters with a |
||||
* statically bounded range are accepted. |
||||
*/ |
||||
|
||||
namespace PhpAot\Php\Optimizer; |
||||
|
||||
use PhpAot\Php\Analysis\SsaFlags; |
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\Node\Stmt; |
||||
use PhpParser\NodeAbstract; |
||||
|
||||
trait LoopVarOptimizer |
||||
{ |
||||
protected const array LOOP_NON_NEGATIVE_INT_FUNCTIONS = [ |
||||
'strlen' => true, |
||||
'count' => true, |
||||
'sizeof' => true, |
||||
]; |
||||
|
||||
protected function optimizeLoopVars(): void |
||||
{ |
||||
$ssa = $this->context->ssaBuilder; |
||||
if (!$ssa) { |
||||
return; |
||||
} |
||||
|
||||
$stmts = $ssa->getStmts(); |
||||
if (!$stmts) { |
||||
return; |
||||
} |
||||
|
||||
$candidates = []; |
||||
$this->collectLoopVarCandidates($stmts, [], $candidates); |
||||
|
||||
foreach ($candidates as $varName => $candidate) { |
||||
$escapedName = $this->escapeVarName($varName); |
||||
if ($this->hasArgument($escapedName) |
||||
|| $this->hasScopeGlobalVar($escapedName) |
||||
|| $this->isSuperGlobal($escapedName)) { |
||||
continue; |
||||
} |
||||
if (!$this->isLoopSsaVarStable($varName)) { |
||||
continue; |
||||
} |
||||
if ($this->loopVarHasUnsafeUsage($varName, $stmts, $candidate['allowed'] ?? [])) { |
||||
continue; |
||||
} |
||||
foreach ($candidate['deps'] ?? [] as $depName => $_) { |
||||
if (!$this->isLoopSsaVarStable($depName) |
||||
|| $this->loopVarHasUnsafeUsage($depName, $stmts, $candidates[$depName]['allowed'] ?? [])) { |
||||
continue 2; |
||||
} |
||||
} |
||||
$this->context->localVars[$escapedName] = self::TYPE_INT; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates |
||||
*/ |
||||
protected function allowLoopNode(array &$candidates, string $varName, int $nodeId): void |
||||
{ |
||||
$candidates[$varName]['allowed'][$nodeId] = true; |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates |
||||
*/ |
||||
protected function requireLoopVar(array &$candidates, string $varName, string $depName): void |
||||
{ |
||||
if ($varName !== $depName) { |
||||
$candidates[$varName]['deps'][$depName] = true; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{id: int}> $safeVars |
||||
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates |
||||
*/ |
||||
protected function collectLoopVarCandidates(array $stmts, array $safeVars, array &$candidates): void |
||||
{ |
||||
foreach ($stmts as $stmt) { |
||||
if ($stmt instanceof Stmt\Expression) { |
||||
$this->trackLoopSafeAssignment($stmt->expr, $safeVars); |
||||
} |
||||
|
||||
if ($stmt instanceof Stmt\While_) { |
||||
$this->tryCollectWhilePostDecCandidate($stmt, $safeVars, $candidates); |
||||
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates); |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Stmt\For_) { |
||||
$this->tryCollectForCounterCandidate($stmt, $safeVars, $candidates); |
||||
$innerSafeVars = $safeVars; |
||||
foreach ($stmt->init as $init) { |
||||
$this->trackLoopSafeAssignment($init, $innerSafeVars); |
||||
} |
||||
$this->collectLoopVarCandidates($stmt->stmts, $innerSafeVars, $candidates); |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Stmt\If_) { |
||||
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates); |
||||
foreach ($stmt->elseifs as $elseif) { |
||||
$this->collectLoopVarCandidates($elseif->stmts, $safeVars, $candidates); |
||||
} |
||||
if ($stmt->else) { |
||||
$this->collectLoopVarCandidates($stmt->else->stmts, $safeVars, $candidates); |
||||
} |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Stmt\Do_) { |
||||
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates); |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Stmt\Foreach_) { |
||||
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates); |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Stmt\Switch_) { |
||||
foreach ($stmt->cases as $case) { |
||||
$this->collectLoopVarCandidates($case->stmts, $safeVars, $candidates); |
||||
} |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Stmt\TryCatch) { |
||||
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates); |
||||
foreach ($stmt->catches as $catch) { |
||||
$this->collectLoopVarCandidates($catch->stmts, $safeVars, $candidates); |
||||
} |
||||
if ($stmt->finally) { |
||||
$this->collectLoopVarCandidates($stmt->finally->stmts, $safeVars, $candidates); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{id: int}> $safeVars |
||||
*/ |
||||
protected function trackLoopSafeAssignment(NodeAbstract $expr, array &$safeVars): void |
||||
{ |
||||
if ($expr instanceof Expr\Assign |
||||
&& $expr->var instanceof Expr\Variable |
||||
&& is_string($expr->var->name)) { |
||||
$varName = $expr->var->name; |
||||
$info = $this->detectLoopIntExprInfo($expr->expr, $safeVars); |
||||
if ($info !== null) { |
||||
$safeVars[$varName] = [ |
||||
'id' => spl_object_id($expr), |
||||
'nonNegative' => $info['nonNegative'], |
||||
'inclusiveSafe' => $info['inclusiveSafe'], |
||||
]; |
||||
} else { |
||||
unset($safeVars[$varName]); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
if ($expr instanceof Expr\AssignOp |
||||
&& $expr->var instanceof Expr\Variable |
||||
&& is_string($expr->var->name)) { |
||||
unset($safeVars[$expr->var->name]); |
||||
return; |
||||
} |
||||
|
||||
if (($expr instanceof Expr\PreInc || $expr instanceof Expr\PostInc |
||||
|| $expr instanceof Expr\PreDec || $expr instanceof Expr\PostDec) |
||||
&& $expr->var instanceof Expr\Variable |
||||
&& is_string($expr->var->name)) { |
||||
unset($safeVars[$expr->var->name]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{id: int}> $safeVars |
||||
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates |
||||
*/ |
||||
protected function tryCollectWhilePostDecCandidate(Stmt\While_ $stmt, array $safeVars, array &$candidates): void |
||||
{ |
||||
if (!$stmt->cond instanceof Expr\PostDec |
||||
|| !$stmt->cond->var instanceof Expr\Variable |
||||
|| !is_string($stmt->cond->var->name)) { |
||||
return; |
||||
} |
||||
|
||||
$varName = $stmt->cond->var->name; |
||||
if (!isset($safeVars[$varName]) || !$safeVars[$varName]['nonNegative']) { |
||||
return; |
||||
} |
||||
|
||||
if ($this->loopBodyMutatesAny($stmt->stmts, [$varName => true])) { |
||||
return; |
||||
} |
||||
|
||||
$this->allowLoopNode($candidates, $varName, $safeVars[$varName]['id']); |
||||
$this->allowLoopNode($candidates, $varName, spl_object_id($stmt->cond)); |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{id: int}> $safeVars |
||||
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates |
||||
*/ |
||||
protected function tryCollectForCounterCandidate(Stmt\For_ $stmt, array $safeVars, array &$candidates): void |
||||
{ |
||||
if (count($stmt->init) !== 1 || count($stmt->cond) !== 1 || count($stmt->loop) !== 1) { |
||||
return; |
||||
} |
||||
|
||||
$init = $stmt->init[0]; |
||||
if (!$init instanceof Expr\Assign |
||||
|| !$init->var instanceof Expr\Variable |
||||
|| !is_string($init->var->name) |
||||
|| $this->detectLoopIntExprInfo($init->expr, $safeVars) === null) { |
||||
return; |
||||
} |
||||
|
||||
$counterName = $init->var->name; |
||||
$step = $this->detectLoopUnitStep($stmt->loop[0], $counterName); |
||||
if ($step === null) { |
||||
return; |
||||
} |
||||
|
||||
$bound = $this->matchLoopBound($stmt->cond[0], $counterName, $safeVars, $step); |
||||
if ($bound === null) { |
||||
return; |
||||
} |
||||
|
||||
$watchedVars = [$counterName => true]; |
||||
foreach ($bound['vars'] as $varName => $_) { |
||||
$watchedVars[$varName] = true; |
||||
} |
||||
if ($this->loopBodyMutatesAny($stmt->stmts, $watchedVars)) { |
||||
return; |
||||
} |
||||
|
||||
$conditionId = spl_object_id($stmt->cond[0]); |
||||
$this->allowLoopNode($candidates, $counterName, spl_object_id($init)); |
||||
$this->allowLoopNode($candidates, $counterName, $conditionId); |
||||
$this->allowLoopNode($candidates, $counterName, spl_object_id($stmt->loop[0])); |
||||
|
||||
foreach ($bound['intVars'] as $varName => $_) { |
||||
$this->requireLoopVar($candidates, $counterName, $varName); |
||||
$this->allowLoopNode($candidates, $varName, $safeVars[$varName]['id']); |
||||
$this->allowLoopNode($candidates, $varName, $conditionId); |
||||
} |
||||
} |
||||
|
||||
protected function detectLoopUnitStep(NodeAbstract $expr, string $counterName): ?int |
||||
{ |
||||
if (($expr instanceof Expr\PostInc || $expr instanceof Expr\PreInc) |
||||
&& $this->isVarNamed($expr->var, $counterName)) { |
||||
return 1; |
||||
} |
||||
|
||||
if (($expr instanceof Expr\PostDec || $expr instanceof Expr\PreDec) |
||||
&& $this->isVarNamed($expr->var, $counterName)) { |
||||
return -1; |
||||
} |
||||
|
||||
if ($expr instanceof Expr\AssignOp\Plus |
||||
&& $this->isVarNamed($expr->var, $counterName) |
||||
&& $expr->expr instanceof Node\Scalar\LNumber |
||||
&& $expr->expr->value === 1) { |
||||
return 1; |
||||
} |
||||
|
||||
if ($expr instanceof Expr\AssignOp\Minus |
||||
&& $this->isVarNamed($expr->var, $counterName) |
||||
&& $expr->expr instanceof Node\Scalar\LNumber |
||||
&& $expr->expr->value === 1) { |
||||
return -1; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{id: int}> $safeVars |
||||
* @return array{vars: array<string, bool>, intVars: array<string, bool>}|null |
||||
*/ |
||||
protected function matchLoopBound(NodeAbstract $expr, string $counterName, array $safeVars, int $step): ?array |
||||
{ |
||||
if (!$expr instanceof Expr\BinaryOp\Smaller |
||||
&& !$expr instanceof Expr\BinaryOp\SmallerOrEqual |
||||
&& !$expr instanceof Expr\BinaryOp\Greater |
||||
&& !$expr instanceof Expr\BinaryOp\GreaterOrEqual) { |
||||
return null; |
||||
} |
||||
|
||||
$inclusive = $expr instanceof Expr\BinaryOp\SmallerOrEqual |
||||
|| $expr instanceof Expr\BinaryOp\GreaterOrEqual; |
||||
$boundExpr = null; |
||||
if ($step > 0) { |
||||
if (($expr instanceof Expr\BinaryOp\Smaller || $expr instanceof Expr\BinaryOp\SmallerOrEqual) |
||||
&& $this->isVarNamed($expr->left, $counterName)) { |
||||
$boundExpr = $expr->right; |
||||
} elseif (($expr instanceof Expr\BinaryOp\Greater || $expr instanceof Expr\BinaryOp\GreaterOrEqual) |
||||
&& $this->isVarNamed($expr->right, $counterName)) { |
||||
$boundExpr = $expr->left; |
||||
} |
||||
} else { |
||||
if (($expr instanceof Expr\BinaryOp\Greater || $expr instanceof Expr\BinaryOp\GreaterOrEqual) |
||||
&& $this->isVarNamed($expr->left, $counterName)) { |
||||
$boundExpr = $expr->right; |
||||
} elseif (($expr instanceof Expr\BinaryOp\Smaller || $expr instanceof Expr\BinaryOp\SmallerOrEqual) |
||||
&& $this->isVarNamed($expr->right, $counterName)) { |
||||
$boundExpr = $expr->left; |
||||
} |
||||
} |
||||
|
||||
if (!$boundExpr instanceof NodeAbstract) { |
||||
return null; |
||||
} |
||||
|
||||
$info = $this->detectLoopIntExprInfo($boundExpr, $safeVars); |
||||
if ($info === null || ($inclusive && !$info['inclusiveSafe'])) { |
||||
return null; |
||||
} |
||||
|
||||
return [ |
||||
'vars' => $this->collectLoopExprVars($boundExpr), |
||||
'intVars' => $this->collectLoopExprSafeIntVars($boundExpr, $safeVars), |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{id: int}> $safeVars |
||||
* @return array{nonNegative: bool, inclusiveSafe: bool}|null |
||||
*/ |
||||
protected function detectLoopIntExprInfo(NodeAbstract $expr, array $safeVars): ?array |
||||
{ |
||||
if ($expr instanceof Node\Scalar\LNumber) { |
||||
return [ |
||||
'nonNegative' => $expr->value >= 0, |
||||
'inclusiveSafe' => $expr->value > PHP_INT_MIN && $expr->value < PHP_INT_MAX, |
||||
]; |
||||
} |
||||
|
||||
if ($expr instanceof Expr\Cast\Int_) { |
||||
return [ |
||||
'nonNegative' => false, |
||||
'inclusiveSafe' => false, |
||||
]; |
||||
} |
||||
|
||||
if ($expr instanceof Expr\Variable && is_string($expr->name)) { |
||||
return $safeVars[$expr->name] ?? null; |
||||
} |
||||
|
||||
if ($this->isLoopIntCall($expr)) { |
||||
$knownNonNegative = $this->isLoopKnownNonNegativeIntCall($expr); |
||||
return [ |
||||
'nonNegative' => $knownNonNegative, |
||||
// PHP lengths/counts are bounded by addressable memory in |
||||
// supported runtimes, so inclusive loops over them stay int. |
||||
'inclusiveSafe' => $knownNonNegative, |
||||
]; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
protected function isLoopIntCall(NodeAbstract $expr): bool |
||||
{ |
||||
if (!$expr instanceof Expr\FuncCall |
||||
&& !$expr instanceof Expr\MethodCall |
||||
&& !$expr instanceof Expr\StaticCall |
||||
&& !$expr instanceof Expr\NullsafeMethodCall) { |
||||
return false; |
||||
} |
||||
|
||||
return $this->detectTypeOfExpr($expr) === self::TYPE_INT; |
||||
} |
||||
|
||||
protected function isLoopKnownNonNegativeIntCall(NodeAbstract $expr): bool |
||||
{ |
||||
return $expr instanceof Expr\FuncCall |
||||
&& $expr->name instanceof Node\Name |
||||
&& isset(self::LOOP_NON_NEGATIVE_INT_FUNCTIONS[strtolower($expr->name->toString())]); |
||||
} |
||||
|
||||
/** |
||||
* @return array<string, bool> |
||||
*/ |
||||
protected function collectLoopExprVars(NodeAbstract $expr): array |
||||
{ |
||||
$vars = []; |
||||
$this->collectLoopExprVarsInto($expr, $vars); |
||||
return $vars; |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, bool> $vars |
||||
*/ |
||||
protected function collectLoopExprVarsInto($node, array &$vars): void |
||||
{ |
||||
if (!$node instanceof Node) { |
||||
return; |
||||
} |
||||
if ($node instanceof Expr\Variable && is_string($node->name)) { |
||||
$vars[$node->name] = true; |
||||
return; |
||||
} |
||||
foreach ($node->getSubNodeNames() as $name) { |
||||
$value = $node->$name; |
||||
if ($value instanceof Node) { |
||||
$this->collectLoopExprVarsInto($value, $vars); |
||||
} elseif (is_array($value)) { |
||||
foreach ($value as $item) { |
||||
$this->collectLoopExprVarsInto($item, $vars); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, array{id: int}> $safeVars |
||||
* @return array<string, bool> |
||||
*/ |
||||
protected function collectLoopExprSafeIntVars(NodeAbstract $expr, array $safeVars): array |
||||
{ |
||||
$vars = []; |
||||
foreach ($this->collectLoopExprVars($expr) as $varName => $_) { |
||||
if (isset($safeVars[$varName])) { |
||||
$vars[$varName] = true; |
||||
} |
||||
} |
||||
return $vars; |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, bool> $vars |
||||
*/ |
||||
protected function loopBodyMutatesAny(array $stmts, array $vars): bool |
||||
{ |
||||
foreach ($stmts as $stmt) { |
||||
if ($this->loopNodeMutatesAny($stmt, $vars)) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, bool> $vars |
||||
*/ |
||||
protected function loopNodeMutatesAny($node, array $vars): bool |
||||
{ |
||||
if (!$node instanceof Node) { |
||||
return false; |
||||
} |
||||
|
||||
if ($node instanceof Expr\Assign || $node instanceof Expr\AssignOp || $node instanceof Expr\AssignRef) { |
||||
if ($this->loopExprTargetsAny($node->var, $vars) |
||||
|| ($node instanceof Expr\AssignRef && $this->loopExprUsesAny($node->expr, $vars))) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
if (($node instanceof Expr\PreInc || $node instanceof Expr\PostInc |
||||
|| $node instanceof Expr\PreDec || $node instanceof Expr\PostDec) |
||||
&& $this->loopExprTargetsAny($node->var, $vars)) { |
||||
return true; |
||||
} |
||||
|
||||
if ($node instanceof Expr\FuncCall || $node instanceof Expr\MethodCall |
||||
|| $node instanceof Expr\StaticCall || $node instanceof Expr\NullsafeMethodCall) { |
||||
foreach ($node->args as $arg) { |
||||
if ($arg instanceof Node\Arg |
||||
&& $arg->byRef |
||||
&& $this->loopExprUsesAny($arg->value, $vars)) { |
||||
return true; |
||||
} |
||||
} |
||||
if ($node instanceof Expr\FuncCall |
||||
&& $node->name instanceof Node\Name |
||||
&& strtolower($node->name->toString()) === 'refval') { |
||||
foreach ($node->args as $arg) { |
||||
if ($arg instanceof Node\Arg && $this->loopExprUsesAny($arg->value, $vars)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
if ($node instanceof Stmt\Unset_) { |
||||
foreach ($node->vars as $var) { |
||||
if ($this->loopExprTargetsAny($var, $vars)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if ($node instanceof Stmt\Foreach_) { |
||||
if ($this->loopExprTargetsAny($node->valueVar, $vars) |
||||
|| ($node->keyVar instanceof Node && $this->loopExprTargetsAny($node->keyVar, $vars))) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
foreach ($node->getSubNodeNames() as $name) { |
||||
$value = $node->$name; |
||||
if ($value instanceof Node) { |
||||
if ($this->loopNodeMutatesAny($value, $vars)) { |
||||
return true; |
||||
} |
||||
} elseif (is_array($value)) { |
||||
foreach ($value as $item) { |
||||
if ($this->loopNodeMutatesAny($item, $vars)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, bool> $vars |
||||
*/ |
||||
protected function loopExprTargetsAny($expr, array $vars): bool |
||||
{ |
||||
while ($expr instanceof Expr\ArrayDimFetch || $expr instanceof Expr\PropertyFetch) { |
||||
$expr = $expr->var; |
||||
} |
||||
return $expr instanceof Expr\Variable |
||||
&& is_string($expr->name) |
||||
&& isset($vars[$expr->name]); |
||||
} |
||||
|
||||
/** |
||||
* @param array<string, bool> $vars |
||||
*/ |
||||
protected function loopExprUsesAny($node, array $vars): bool |
||||
{ |
||||
if (!$node instanceof Node) { |
||||
return false; |
||||
} |
||||
if ($node instanceof Expr\Variable && is_string($node->name) && isset($vars[$node->name])) { |
||||
return true; |
||||
} |
||||
foreach ($node->getSubNodeNames() as $name) { |
||||
$value = $node->$name; |
||||
if ($value instanceof Node) { |
||||
if ($this->loopExprUsesAny($value, $vars)) { |
||||
return true; |
||||
} |
||||
} elseif (is_array($value)) { |
||||
foreach ($value as $item) { |
||||
if ($this->loopExprUsesAny($item, $vars)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected function isLoopSsaVarStable(string $varName): bool |
||||
{ |
||||
$ssa = $this->context->ssaBuilder; |
||||
if (!$ssa) { |
||||
return false; |
||||
} |
||||
|
||||
foreach ($ssa->ssaVars as $ssaVar) { |
||||
if ($ssaVar->origName !== $varName) { |
||||
continue; |
||||
} |
||||
if ($ssaVar->flags & (SsaFlags::REFERENCE | SsaFlags::ESCAPED | SsaFlags::KILLED)) { |
||||
return false; |
||||
} |
||||
} |
||||
// SsaBuilder does not currently materialize all variables defined only |
||||
// in `for` headers. Those candidates are still checked by the AST |
||||
// whitelist and whole-function hazard scan in this optimizer. |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @param array<int, bool> $allowedNodes |
||||
*/ |
||||
protected function loopVarHasUnsafeUsage(string $varName, array $stmts, array $allowedNodes): bool |
||||
{ |
||||
foreach ($stmts as $stmt) { |
||||
if ($this->loopNodeHasIntHazard($stmt, $varName, $allowedNodes)) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* @param array<int, bool> $allowedNodes |
||||
*/ |
||||
protected function loopNodeHasIntHazard($node, string $varName, array $allowedNodes): bool |
||||
{ |
||||
if (!$node instanceof Node) { |
||||
return false; |
||||
} |
||||
if (isset($allowedNodes[spl_object_id($node)])) { |
||||
return false; |
||||
} |
||||
|
||||
if ($this->loopExprHasIntHazard($node, $varName, $allowedNodes)) { |
||||
return true; |
||||
} |
||||
|
||||
foreach ($node->getSubNodeNames() as $name) { |
||||
$value = $node->$name; |
||||
if ($value instanceof Node) { |
||||
if ($this->loopNodeHasIntHazard($value, $varName, $allowedNodes)) { |
||||
return true; |
||||
} |
||||
} elseif (is_array($value)) { |
||||
foreach ($value as $item) { |
||||
if ($this->loopNodeHasIntHazard($item, $varName, $allowedNodes)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* @param array<int, bool> $allowedNodes |
||||
*/ |
||||
protected function loopExprHasIntHazard($expr, string $varName, array $allowedNodes): bool |
||||
{ |
||||
if (!$expr instanceof Node) { |
||||
return false; |
||||
} |
||||
if (isset($allowedNodes[spl_object_id($expr)])) { |
||||
return false; |
||||
} |
||||
|
||||
if ($expr instanceof Expr\BinaryOp) { |
||||
if (isset(self::SAFE_INT_BINARY_OPS[$expr->getType()])) { |
||||
return $this->loopExprHasIntHazard($expr->left, $varName, $allowedNodes) |
||||
|| $this->loopExprHasIntHazard($expr->right, $varName, $allowedNodes); |
||||
} |
||||
return $this->exprUsesVar($expr, $varName); |
||||
} |
||||
|
||||
if ($expr instanceof Expr\Assign) { |
||||
if ($this->isVarNamed($expr->var, $varName)) { |
||||
return true; |
||||
} |
||||
return $this->loopExprHasIntHazard($expr->expr, $varName, $allowedNodes); |
||||
} |
||||
|
||||
if ($expr instanceof Expr\AssignRef) { |
||||
return $this->isVarNamed($expr->var, $varName) |
||||
|| $this->exprUsesVar($expr->expr, $varName); |
||||
} |
||||
|
||||
if ($expr instanceof Expr\AssignOp) { |
||||
if ($this->isVarNamed($expr->var, $varName)) { |
||||
return true; |
||||
} |
||||
return $this->loopExprHasIntHazard($expr->expr, $varName, $allowedNodes); |
||||
} |
||||
|
||||
if ($expr instanceof Expr\PreInc || $expr instanceof Expr\PreDec |
||||
|| $expr instanceof Expr\PostInc || $expr instanceof Expr\PostDec) { |
||||
return $this->isVarNamed($expr->var, $varName); |
||||
} |
||||
|
||||
if ($expr instanceof Expr\UnaryMinus) { |
||||
return $this->exprUsesVar($expr, $varName); |
||||
} |
||||
|
||||
if ($expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall |
||||
|| $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) { |
||||
foreach ($expr->args as $arg) { |
||||
if ($arg instanceof Node\Arg |
||||
&& $arg->byRef |
||||
&& $this->exprUsesVar($arg->value, $varName)) { |
||||
return true; |
||||
} |
||||
} |
||||
if ($expr instanceof Expr\FuncCall |
||||
&& $expr->name instanceof Node\Name |
||||
&& strtolower($expr->name->toString()) === 'refval') { |
||||
foreach ($expr->args as $arg) { |
||||
if ($arg instanceof Node\Arg && $this->exprUsesVar($arg->value, $varName)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
if ($expr instanceof Stmt\Unset_) { |
||||
foreach ($expr->vars as $var) { |
||||
if ($this->isVarNamed($var, $varName)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
foreach ($expr->getSubNodeNames() as $name) { |
||||
$value = $expr->$name; |
||||
if ($value instanceof Node) { |
||||
if ($this->loopExprHasIntHazard($value, $varName, $allowedNodes)) { |
||||
return true; |
||||
} |
||||
} elseif (is_array($value)) { |
||||
foreach ($value as $item) { |
||||
if ($this->loopExprHasIntHazard($item, $varName, $allowedNodes)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
--TEST-- |
||||
SSA narrowing: nested integer-only ops on float prevent narrowing |
||||
--FILE-- |
||||
<?php |
||||
function main(): void { |
||||
$x = 6.7; |
||||
$y = $x & 3; |
||||
var_dump($y); |
||||
|
||||
$z = 10.5; |
||||
var_dump($z % 4); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(2) |
||||
int(2) |
||||
@ -0,0 +1,23 @@ |
||||
--TEST-- |
||||
Loop var optimizer: for counter with constant bound variable |
||||
--FILE-- |
||||
<?php |
||||
function main(): void { |
||||
$n = 20000; |
||||
$hits = 0; |
||||
|
||||
for ($i = 0; $i < $n; $i++) { |
||||
if ($i === 0 || $i === 19999) { |
||||
$hits++; |
||||
} |
||||
} |
||||
|
||||
var_dump($i); |
||||
var_dump($n); |
||||
var_dump($hits); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(20000) |
||||
int(20000) |
||||
int(2) |
||||
@ -0,0 +1,22 @@ |
||||
--TEST-- |
||||
Loop var optimizer: for counter with typed int function bound |
||||
--FILE-- |
||||
<?php |
||||
function limit_value(): int { |
||||
return 5; |
||||
} |
||||
|
||||
function main(): void { |
||||
$last = -1; |
||||
|
||||
for ($i = 0; $i < limit_value(); $i++) { |
||||
$last = $i; |
||||
} |
||||
|
||||
var_dump($i); |
||||
var_dump($last); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(5) |
||||
int(4) |
||||
@ -0,0 +1,22 @@ |
||||
--TEST-- |
||||
Loop var optimizer: descending for counter with typed int function init |
||||
--FILE-- |
||||
<?php |
||||
function start_value(): int { |
||||
return 4; |
||||
} |
||||
|
||||
function main(): void { |
||||
$sum = 0; |
||||
|
||||
for ($i = start_value(); $i > 0; $i--) { |
||||
$sum += $i; |
||||
} |
||||
|
||||
var_dump($i); |
||||
var_dump($sum); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(0) |
||||
int(10) |
||||
@ -0,0 +1,19 @@ |
||||
--TEST-- |
||||
Loop var optimizer: for counter with strlen bound |
||||
--FILE-- |
||||
<?php |
||||
function main(): void { |
||||
$s = "abcdef"; |
||||
$last = -1; |
||||
|
||||
for ($i = 0; $i <= strlen($s); $i++) { |
||||
$last = $i; |
||||
} |
||||
|
||||
var_dump($i); |
||||
var_dump($last); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(7) |
||||
int(6) |
||||
@ -0,0 +1,21 @@ |
||||
--TEST-- |
||||
Loop var optimizer: while post-decrement from constant |
||||
--FILE-- |
||||
<?php |
||||
function main(): void { |
||||
$n = 1000; |
||||
$sum = 0; |
||||
|
||||
while ($n--) { |
||||
if ($n < 3) { |
||||
$sum += $n; |
||||
} |
||||
} |
||||
|
||||
var_dump($n); |
||||
var_dump($sum); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(-1) |
||||
int(3) |
||||
@ -0,0 +1,30 @@ |
||||
--TEST-- |
||||
SSA object prop: loop body object redefinition prevents hoisting |
||||
--FILE-- |
||||
<?php |
||||
use native_types; |
||||
|
||||
class Foo { |
||||
public int $a; |
||||
} |
||||
|
||||
function readFoo(Foo $foo): int { |
||||
return $foo->a; |
||||
} |
||||
|
||||
function main(): void { |
||||
$o = new Foo(); |
||||
$o->a = 1; |
||||
|
||||
$n = 1; |
||||
while ($n--) { |
||||
$o = new Foo(); |
||||
$o->a = 5; |
||||
} |
||||
|
||||
$o->a += 1; |
||||
var_dump(readFoo($o)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(6) |
||||
@ -0,0 +1,27 @@ |
||||
--TEST-- |
||||
SSA object prop: nested refval property use prevents hoisting |
||||
--FILE-- |
||||
<?php |
||||
use native_types; |
||||
|
||||
class Foo { |
||||
public int $a; |
||||
} |
||||
|
||||
function mutate(&$value): int { |
||||
$value = 20; |
||||
return 1; |
||||
} |
||||
|
||||
function main(): void { |
||||
$o = new Foo(); |
||||
$o->a = 10; |
||||
|
||||
$ignored = mutate(refval($o->a)); |
||||
$o->a += 5; |
||||
|
||||
var_dump($o->a); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(25) |
||||
Loading…
Reference in new issue