feat(compiler): 添加循环变量优化器以提升整数类型推断

- 在CompilerBase中集成LoopVarOptimizer trait
- 实现range-proven循环变量优化算法,支持while和for循环
- 添加多项式时间复杂度的循环计数器类型窄化功能
- 集成安全检查机制防止不安全的整数运算推导
- 添加针对strlen、count等函数的非负整数类型识别
- 实现循环边界安全检测避免溢出风险
- 新增多种循环模式的测试用例验证优化效果
- 更新现有测试注释以反映新的类型推断行为
pull/1/head
韩天峰 3 months ago
parent cdd0b05bf7
commit dbaa5a10e0
  1. 82
      phpunit/src/Analysis/SsaBuilderTest.php
  2. 265
      phpunit/src/SsaAnalysisTest.php
  3. 203
      src/Php/Analysis/SsaBuilder.php
  4. 4
      src/Php/CompilerBase.php
  5. 734
      src/Php/Optimizer/LoopVarOptimizer.php
  6. 126
      src/Php/Optimizer/SsaPropOptimizer.php
  7. 223
      src/Php/Optimizer/SsaTypeOptimizer.php
  8. 2
      src/Php/Translator.php
  9. 3
      tests/aot/optimizations/float-narrow-mixed.phpt
  10. 16
      tests/aot/optimizations/float-narrow-nested-int-ops.phpt
  11. 2
      tests/aot/optimizations/int-narrow-overflow.phpt
  12. 23
      tests/aot/optimizations/loop-var-for-const-bound.phpt
  13. 22
      tests/aot/optimizations/loop-var-for-int-function-bound.phpt
  14. 22
      tests/aot/optimizations/loop-var-for-int-function-desc.phpt
  15. 19
      tests/aot/optimizations/loop-var-for-strlen-bound.phpt
  16. 21
      tests/aot/optimizations/loop-var-while-postdec.phpt
  17. 3
      tests/aot/optimizations/narrow-div-pow.phpt
  18. 2
      tests/aot/optimizations/narrow-ref-prevent.phpt
  19. 30
      tests/aot/optimizations/objprop-hoist-loop-redef.phpt
  20. 27
      tests/aot/optimizations/objprop-hoist-nested-refval.phpt

@ -154,6 +154,19 @@ class SsaBuilderTest extends TestCase
$this->assertCount(2, $xVars, 'Two assignments to $x should create two SSA vars');
}
public function testAssignOpAndIncDecCreateNewSsaVars(): void
{
$builder = $this->buildSsa('$y = 1; $y &= 3; $y++; --$y;');
$yVars = [];
foreach ($builder->ssaVars as $var) {
if ($var->origName === 'y' && !($var->flags & SsaFlags::PHI)) {
$yVars[] = $var;
}
}
$this->assertCount(4, $yVars, 'Assignment, compound assignment, and inc/dec should each define $y');
}
public function testVarDefBlocks(): void
{
$builder = $this->buildSsa('$a = 1; $b = 2;');
@ -199,6 +212,21 @@ class SsaBuilderTest extends TestCase
}
}
public function testImmediateDominatorUsesClosestDominator(): void
{
$builder = $this->buildSsa('
$a = 1;
target:
$b = 2;
');
$exitBlock = $builder->blocks[count($builder->blocks) - 1];
$labelBlockId = $builder->getLabelBlock('target');
$this->assertNotNull($labelBlockId);
$this->assertSame($labelBlockId, $exitBlock->dominator);
}
public function testPhiFunctionPlacedAtJoin(): void
{
$builder = $this->buildSsa('
@ -282,6 +310,40 @@ class SsaBuilderTest extends TestCase
$this->assertNotNull($escapedVar, 'Call by reference should create ESCAPED SSA var');
}
public function testNestedCallByRefCreatesEscapedVar(): void
{
$builder = $this->buildSsa('
$y = some_func(refval($x));
');
$escapedVar = null;
foreach ($builder->ssaVars as $var) {
if ($var->origName === 'x' && ($var->flags & SsaFlags::ESCAPED)) {
$escapedVar = $var;
break;
}
}
$this->assertNotNull($escapedVar, 'Nested refval($x) should create ESCAPED SSA var');
}
public function testCallByRefInsideLoopCreatesEscapedVar(): void
{
$builder = $this->buildSsa('
while ($x) {
some_func(refval($x));
}
');
$escapedVar = null;
foreach ($builder->ssaVars as $var) {
if ($var->origName === 'x' && ($var->flags & SsaFlags::ESCAPED)) {
$escapedVar = $var;
break;
}
}
$this->assertNotNull($escapedVar, 'refval($x) inside loop body should create ESCAPED SSA var');
}
public function testRefvalCallByRefCreatesEscapedVar(): void
{
// refval() is the AOT compiler's pseudo-function for dynamic call reference passing
@ -433,6 +495,26 @@ class SsaBuilderTest extends TestCase
$this->assertTrue($hasValue, 'foreach value variable should be defined');
}
public function testLoopBodyAssignmentsCreateSsaVars(): void
{
$builder = $this->buildSsa('
$obj = new Foo();
while ($x) {
$obj = new Foo();
}
');
$objVars = [];
foreach ($builder->ssaVars as $var) {
if ($var->origName === 'obj' && !($var->flags & SsaFlags::PHI)) {
$objVars[] = $var;
}
}
$this->assertCount(2, $objVars, 'Assignment inside loop body should be tracked as an SSA definition');
$this->assertContains(0, $builder->getDefBlocks('obj'));
}
public function testStaticVariableIsEscaped(): void
{
$builder = $this->buildSsa('

@ -15,9 +15,11 @@ use PhpAot\Php\Entity\ClassDef;
use PhpAot\Php\Entity\MethodDef;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt;
use PhpParser\Node\Arg;
use PhpParser\Node\Scalar;
use PhpParser\ParserFactory;
class SsaAnalysisTest extends TestCase
{
@ -92,6 +94,25 @@ class SsaAnalysisTest extends TestCase
$prop->setValue($context, $value);
}
private function optimizeLoopVarsForCode(string $code): array
{
$parser = (new ParserFactory())->createForHostVersion();
$stmts = $parser->parse('<?php function f($s = "") { ' . $code . ' }');
$this->assertNotNull($stmts);
$fn = $stmts[0];
$this->assertInstanceOf(FunctionLike::class, $fn);
$this->invoke('resetFunction');
$builder = new SsaBuilder($fn->getStmts() ?: [], []);
$builder->build();
$this->setContextProperty('ssaBuilder', $builder);
$this->invoke('optimizeLoopVars');
return $this->getContextProperty('localVars');
}
// ========================================================================
// SsaFlags: constant values
// ========================================================================
@ -699,6 +720,27 @@ class SsaAnalysisTest extends TestCase
$this->assertTrue($result, 'unset inside if should be detected');
}
public function testHasDangerousPropOpsNestedRefvalInAssignment(): void
{
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop');
$refvalCall = new Expr\FuncCall(new Node\Name('refval'), [new Arg($propFetch)]);
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($refvalCall)]);
$stmt = new Stmt\Expression(new Expr\Assign(new Expr\Variable('result'), $funcCall));
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]);
$this->assertTrue($result, 'refval($obj->prop) nested in an assignment RHS should be detected');
}
public function testHasDangerousPropOpsNestedByRefInReturn(): void
{
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop');
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($propFetch, true)]);
$stmt = new Stmt\Return_($funcCall);
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]);
$this->assertTrue($result, 'By-ref property argument nested in return should be detected');
}
// ========================================================================
// SsaPropOptimizer: isObjectSsaStable
// ========================================================================
@ -776,6 +818,143 @@ class SsaAnalysisTest extends TestCase
$this->assertFalse($this->compiler->isStableObject('unknown'));
}
// ========================================================================
// LoopVarOptimizer: range-proven counters
// ========================================================================
public function testLoopVarOptimizerNarrowsWhilePostDecFromConstant(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 1000;
while ($n--) {
echo $n;
}
');
$this->assertSame(CompilerBase::TYPE_INT, $locals['n'] ?? null);
}
public function testLoopVarOptimizerNarrowsForCounterAndConstantBoundVar(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 20000;
for ($i = 0; $i < $n; $i++) {
echo $i;
}
');
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
$this->assertSame(CompilerBase::TYPE_INT, $locals['n'] ?? null);
}
public function testLoopVarOptimizerNarrowsForCounterWithStrlenBound(): void
{
$locals = $this->optimizeLoopVarsForCode('
for ($i = 0; $i <= strlen($s); $i++) {
echo $i;
}
');
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
}
public function testLoopVarOptimizerNarrowsForCounterWithGenericIntFunctionBound(): void
{
$locals = $this->optimizeLoopVarsForCode('
for ($i = 0; $i < time(); $i++) {
echo $i;
}
');
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
}
public function testLoopVarOptimizerRejectsInclusiveGenericIntFunctionBound(): void
{
$locals = $this->optimizeLoopVarsForCode('
for ($i = 0; $i <= time(); $i++) {
echo $i;
}
');
$this->assertArrayNotHasKey('i', $locals);
}
public function testLoopVarOptimizerNarrowsDescendingCounterFromIntFunction(): void
{
$locals = $this->optimizeLoopVarsForCode('
for ($i = time(); $i > 0; $i--) {
echo $i;
}
');
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
}
public function testLoopVarOptimizerNarrowsDescendingCounterFromIntMethod(): void
{
$locals = $this->optimizeLoopVarsForCode('
for ($i = $v->toInt(); $i > 0; --$i) {
echo $i;
}
');
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
}
public function testLoopVarOptimizerRejectsBodyCounterMutation(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 10;
for ($i = 0; $i < $n; $i++) {
$i += 2;
}
');
$this->assertArrayNotHasKey('i', $locals);
}
public function testLoopVarOptimizerRejectsNonUnitStep(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 10;
for ($i = 0; $i < $n; $i += 2) {
echo $i;
}
');
$this->assertArrayNotHasKey('i', $locals);
}
public function testLoopVarOptimizerRejectsUnsafeUseAfterLoop(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 10;
for ($i = 0; $i < $n; $i++) {
echo $i;
}
$x = $i + PHP_INT_MAX;
');
$this->assertArrayNotHasKey('i', $locals);
}
public function testLoopVarOptimizerRejectsCounterWhenBoundVarCanChangeBeforeLoop(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 10;
if (time()) {
$n = "not-int";
}
for ($i = 0; $i < $n; $i++) {
echo $i;
}
');
$this->assertArrayNotHasKey('i', $locals);
$this->assertArrayNotHasKey('n', $locals);
}
// ========================================================================
// SsaTypeOptimizer: detectSsaDefType
// ========================================================================
@ -839,6 +1018,58 @@ class SsaAnalysisTest extends TestCase
$this->assertNull($result, 'Foreach variable definition type is unknown');
}
public function testDetectSsaDefTypeAssignOpModIsUnknown(): void
{
$ssaVar = new SsaVar(1, 'x');
$assignOp = new Expr\AssignOp\Mod(new Expr\Variable('x'), new Scalar\LNumber(10));
$ssaVar->definition = new Stmt\Expression($assignOp);
$result = $this->invoke('detectSsaDefType', $ssaVar);
$this->assertNull($result, 'Compound assignment should not prove a narrow int type');
}
public function testDetectSsaDefTypeAssignOpDiv(): void
{
$ssaVar = new SsaVar(1, 'x');
$assignOp = new Expr\AssignOp\Div(new Expr\Variable('x'), new Scalar\LNumber(2));
$ssaVar->definition = new Stmt\Expression($assignOp);
$result = $this->invoke('detectSsaDefType', $ssaVar);
$this->assertEquals(CompilerBase::TYPE_FLOAT, $result);
}
public function testDetectSsaDefTypeAssignOpPlusIsUnknownForIntRhs(): void
{
$ssaVar = new SsaVar(1, 'x');
$assignOp = new Expr\AssignOp\Plus(new Expr\Variable('x'), new Scalar\LNumber(1));
$ssaVar->definition = new Stmt\Expression($assignOp);
$result = $this->invoke('detectSsaDefType', $ssaVar);
$this->assertNull($result, 'Int += can overflow at runtime, so SSA should not infer a narrow int type');
}
public function testDetectSsaDefTypeArithmeticIntExprIsUnknown(): void
{
$ssaVar = new SsaVar(1, 'x');
$expr = new Expr\BinaryOp\Plus(new Scalar\LNumber(1), new Scalar\LNumber(2));
$assign = new Expr\Assign(new Expr\Variable('x'), $expr);
$ssaVar->definition = new Stmt\Expression($assign);
$result = $this->invoke('detectSsaDefType', $ssaVar);
$this->assertNull($result, 'Even constant + is not used for SSA int narrowing without a range-proven fold');
}
public function testDetectSsaDefTypeLiteralBitwiseIntExpr(): void
{
$ssaVar = new SsaVar(1, 'x');
$expr = new Expr\BinaryOp\BitwiseAnd(new Scalar\LNumber(7), new Scalar\LNumber(3));
$assign = new Expr\Assign(new Expr\Variable('x'), $expr);
$ssaVar->definition = new Stmt\Expression($assign);
$result = $this->invoke('detectSsaDefType', $ssaVar);
$this->assertEquals(CompilerBase::TYPE_INT, $result);
}
// ========================================================================
// SsaTypeOptimizer: exprCanOverflowInt
// ========================================================================
@ -867,7 +1098,7 @@ class SsaAnalysisTest extends TestCase
new Scalar\LNumber(1),
new Scalar\LNumber(2)
);
$this->assertFalse($this->invoke('exprCanOverflowInt', $plus), 'Constant 1+2 should not overflow');
$this->assertTrue($this->invoke('exprCanOverflowInt', $plus), 'Arithmetic is treated as unsafe without range proof');
}
public function testExprCanOverflowIntPow(): void
@ -940,6 +1171,16 @@ class SsaAnalysisTest extends TestCase
$this->assertFalse($result, 'Bitwise AND should be safe');
}
public function testHasDangerousIntOpsComparison(): void
{
$var = new Expr\Variable('x');
$comparison = new Expr\BinaryOp\Greater($var, new Scalar\LNumber(0));
$stmt = new Stmt\Expression($comparison);
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
$this->assertFalse($result, 'Comparison reads an int but cannot overflow it');
}
public function testHasDangerousIntOpsAssignOpPlus(): void
{
$var = new Expr\Variable('x');
@ -957,7 +1198,7 @@ class SsaAnalysisTest extends TestCase
$stmt = new Stmt\Expression($andEq);
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
$this->assertFalse($result, 'AssignOp\BitwiseAnd should be safe');
$this->assertTrue($result, 'Compound assignment should be treated as dangerous for int narrowing');
}
public function testHasDangerousIntOpsIncrement(): void
@ -1066,6 +1307,26 @@ class SsaAnalysisTest extends TestCase
$this->assertTrue($result, 'Bitwise assign-op on float should be dangerous');
}
public function testHasDangerousFloatOpsNestedBitwiseBinary(): void
{
$var = new Expr\Variable('x');
$bitwise = new Expr\BinaryOp\BitwiseAnd($var, new Scalar\LNumber(1));
$stmt = new Stmt\Expression(new Expr\Assign(new Expr\Variable('y'), $bitwise));
$result = $this->invoke('hasDangerousFloatOps', 'x', [$stmt]);
$this->assertTrue($result, 'Bitwise binary op nested in an assignment should be dangerous for float');
}
public function testHasDangerousFloatOpsReturnMod(): void
{
$var = new Expr\Variable('x');
$mod = new Expr\BinaryOp\Mod($var, new Scalar\LNumber(2));
$stmt = new Stmt\Return_($mod);
$result = $this->invoke('hasDangerousFloatOps', 'x', [$stmt]);
$this->assertTrue($result, 'Modulo in return should be dangerous for float');
}
public function testHasDangerousFloatOpsArithmetic(): void
{
$var = new Expr\Variable('x');

@ -769,8 +769,8 @@ class SsaBuilder
}
/**
* Find the immediate dominator: the strict dominator that is NOT
* dominated by any other strict dominator of b.
* Find the immediate dominator: the strict dominator that is dominated by
* all other strict dominators of b (the closest dominator to b).
*/
private function findImmediateDominator(int $b, array $strictDom, array $dominators): int
{
@ -778,16 +778,15 @@ class SsaBuilder
return $strictDom[0];
}
// idom is the one not dominated by any other strict dominator
foreach ($strictDom as $candidate) {
$dominatedByOther = false;
$dominatedByAllOthers = true;
foreach ($strictDom as $other) {
if ($other !== $candidate && isset($dominators[$candidate][$other])) {
$dominatedByOther = true;
if ($other !== $candidate && !isset($dominators[$candidate][$other])) {
$dominatedByAllOthers = false;
break;
}
}
if (!$dominatedByOther) {
if ($dominatedByAllOthers) {
return $candidate;
}
}
@ -970,11 +969,23 @@ class SsaBuilder
if ($var instanceof Expr\Variable && is_string($var->name)) {
$defs[] = $var->name;
}
} elseif ($stmt instanceof Stmt\Expression && $stmt->expr instanceof Expr\AssignOp) {
$var = $stmt->expr->var;
if ($var instanceof Expr\Variable && is_string($var->name)) {
$defs[] = $var->name;
}
} elseif ($stmt instanceof Stmt\Expression && $stmt->expr instanceof Expr\AssignRef) {
$var = $stmt->expr->var;
if ($var instanceof Expr\Variable && is_string($var->name)) {
$defs[] = $var->name;
}
} elseif ($stmt instanceof Stmt\Expression
&& ($stmt->expr instanceof Expr\PreInc || $stmt->expr instanceof Expr\PostInc
|| $stmt->expr instanceof Expr\PreDec || $stmt->expr instanceof Expr\PostDec)) {
$var = $stmt->expr->var;
if ($var instanceof Expr\Variable && is_string($var->name)) {
$defs[] = $var->name;
}
} elseif ($stmt instanceof Stmt\Static_) {
foreach ($stmt->vars as $staticVar) {
$defs[] = $staticVar->var->name;
@ -1001,13 +1012,44 @@ class SsaBuilder
}
// Recurse into compound statements for nested definitions
if ($stmt instanceof Stmt\While_ || $stmt instanceof Stmt\Do_ || $stmt instanceof Stmt\For_) {
foreach ($stmt->stmts as $nestedStmt) {
$defs = array_merge($defs, $this->getDefinedVars($nestedStmt));
}
}
if ($stmt instanceof Stmt\Foreach_) {
foreach ($stmt->stmts as $nestedStmt) {
$defs = array_merge($defs, $this->getDefinedVars($nestedStmt));
}
}
if ($stmt instanceof Stmt\Switch_) {
foreach ($stmt->cases as $case) {
foreach ($case->stmts as $nestedStmt) {
$defs = array_merge($defs, $this->getDefinedVars($nestedStmt));
}
}
}
if ($stmt instanceof Stmt\TryCatch) {
foreach ($stmt->stmts as $nestedStmt) {
$defs = array_merge($defs, $this->getDefinedVars($nestedStmt));
}
foreach ($stmt->catches as $catch) {
$defs = array_merge($defs, $this->getDefinedVars($catch));
foreach ($catch->stmts as $nestedStmt) {
$defs = array_merge($defs, $this->getDefinedVars($nestedStmt));
}
}
if ($stmt->finally) {
foreach ($stmt->finally->stmts as $nestedStmt) {
$defs = array_merge($defs, $this->getDefinedVars($nestedStmt));
}
}
}
return $defs;
return array_values(array_unique($defs));
}
// =========================================================================
@ -1116,6 +1158,8 @@ class SsaBuilder
*/
private function renameDefs(Node $stmt, int $blockId, int $stmtIndex, array &$pushedVars): void
{
$this->handleCallByRef($stmt, $pushedVars);
// Handle unset($var) — kill the variable
if ($stmt instanceof Stmt\Unset_) {
foreach ($stmt->vars as $var) {
@ -1192,6 +1236,70 @@ class SsaBuilder
return;
}
// Handle compound assignment: $x += expr, $x %= expr, ...
if ($stmt instanceof Stmt\Expression && $stmt->expr instanceof Expr\AssignOp) {
$var = $stmt->expr->var;
if ($var instanceof Expr\Variable && is_string($var->name)) {
$varName = $var->name;
$ssaId = $this->allocateSsaId();
$ssaVar = new SsaVar($ssaId, $varName, 0);
$ssaVar->definition = $stmt;
$this->ssaVars[$ssaId] = $ssaVar;
if (!isset($this->varStates[$varName])) {
$this->varStates[$varName] = new VarState();
}
$this->varStates[$varName]->stack[] = $ssaId;
$pushedVars[] = [$varName, 'assign_op'];
}
return;
}
// Handle increment/decrement: ++$x, $x++, --$x, $x--
if ($stmt instanceof Stmt\Expression
&& ($stmt->expr instanceof Expr\PreInc || $stmt->expr instanceof Expr\PostInc
|| $stmt->expr instanceof Expr\PreDec || $stmt->expr instanceof Expr\PostDec)) {
$var = $stmt->expr->var;
if ($var instanceof Expr\Variable && is_string($var->name)) {
$varName = $var->name;
$ssaId = $this->allocateSsaId();
$ssaVar = new SsaVar($ssaId, $varName, 0);
$ssaVar->definition = $stmt;
$this->ssaVars[$ssaId] = $ssaVar;
if (!isset($this->varStates[$varName])) {
$this->varStates[$varName] = new VarState();
}
$this->varStates[$varName]->stack[] = $ssaId;
$pushedVars[] = [$varName, 'inc_dec'];
}
return;
}
// Handle loop bodies that are not expanded into separate CFG blocks.
if ($stmt instanceof Stmt\While_ || $stmt instanceof Stmt\Do_) {
foreach ($stmt->stmts as $nestedStmt) {
$this->renameDefs($nestedStmt, $blockId, $stmtIndex, $pushedVars);
}
return;
}
if ($stmt instanceof Stmt\For_) {
foreach ($stmt->stmts as $nestedStmt) {
$this->renameDefs($nestedStmt, $blockId, $stmtIndex, $pushedVars);
}
return;
}
if ($stmt instanceof Stmt\Switch_) {
foreach ($stmt->cases as $case) {
foreach ($case->stmts as $nestedStmt) {
$this->renameDefs($nestedStmt, $blockId, $stmtIndex, $pushedVars);
}
}
return;
}
// Handle foreach value/key variables
if ($stmt instanceof Stmt\Foreach_) {
if ($stmt->valueVar instanceof Expr\Variable && is_string($stmt->valueVar->name)) {
@ -1221,6 +1329,9 @@ class SsaBuilder
$this->varStates[$varName]->stack[] = $ssaId;
$pushedVars[] = [$varName, 'foreach_key'];
}
foreach ($stmt->stmts as $nestedStmt) {
$this->renameDefs($nestedStmt, $blockId, $stmtIndex, $pushedVars);
}
return;
}
@ -1262,13 +1373,21 @@ class SsaBuilder
// Handle TryCatch: process catch variables as definitions
if ($stmt instanceof Stmt\TryCatch) {
foreach ($stmt->stmts as $nestedStmt) {
$this->renameDefs($nestedStmt, $blockId, $stmtIndex, $pushedVars);
}
foreach ($stmt->catches as $catch) {
$this->renameDefs($catch, $blockId, $stmtIndex, $pushedVars);
foreach ($catch->stmts as $nestedStmt) {
$this->renameDefs($nestedStmt, $blockId, $stmtIndex, $pushedVars);
}
}
if ($stmt->finally) {
foreach ($stmt->finally->stmts as $nestedStmt) {
$this->renameDefs($nestedStmt, $blockId, $stmtIndex, $pushedVars);
}
}
}
// Handle function calls that may modify variables by reference
$this->handleCallByRef($stmt, $pushedVars);
}
/**
@ -1287,17 +1406,67 @@ class SsaBuilder
*/
private function handleCallByRef(Node $stmt, array &$pushedVars): void
{
if (!($stmt instanceof Stmt\Expression)) {
if ($stmt instanceof Stmt\Expression) {
$this->handleCallByRefInExpr($stmt->expr, $stmt, $pushedVars);
return;
}
$expr = $stmt->expr;
if ($expr instanceof Expr\FuncCall) {
$this->collectCallByRefArgs($expr->args, $stmt, $pushedVars);
if (($stmt instanceof Stmt\If_
|| $stmt instanceof Stmt\While_
|| $stmt instanceof Stmt\Do_)
&& $stmt->cond instanceof Node) {
$this->handleCallByRefInExpr($stmt->cond, $stmt, $pushedVars);
}
if ($stmt instanceof Stmt\For_) {
foreach ([$stmt->init, $stmt->cond, $stmt->loop] as $exprList) {
foreach ($exprList as $expr) {
if ($expr instanceof Node) {
$this->handleCallByRefInExpr($expr, $stmt, $pushedVars);
}
}
}
}
if ($stmt instanceof Stmt\Foreach_ && $stmt->expr instanceof Node) {
$this->handleCallByRefInExpr($stmt->expr, $stmt, $pushedVars);
}
if ($stmt instanceof Stmt\Switch_ && $stmt->cond instanceof Node) {
$this->handleCallByRefInExpr($stmt->cond, $stmt, $pushedVars);
}
if ($stmt instanceof Stmt\Return_ && $stmt->expr instanceof Node) {
$this->handleCallByRefInExpr($stmt->expr, $stmt, $pushedVars);
}
if ($stmt instanceof Stmt\Echo_) {
foreach ($stmt->exprs as $expr) {
if ($expr instanceof Node) {
$this->handleCallByRefInExpr($expr, $stmt, $pushedVars);
}
}
}
}
if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) {
$this->collectCallByRefArgs($expr->args, $stmt, $pushedVars);
private function handleCallByRefInExpr(Node $expr, Node $callStmt, array &$pushedVars): void
{
if ($expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall
|| $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) {
$this->collectCallByRefArgs($expr->args, $callStmt, $pushedVars);
}
foreach ($expr->getSubNodeNames() as $subNodeName) {
$subNode = $expr->$subNodeName;
if ($subNode instanceof Node) {
$this->handleCallByRefInExpr($subNode, $callStmt, $pushedVars);
} elseif (is_array($subNode)) {
foreach ($subNode as $item) {
if ($item instanceof Node) {
$this->handleCallByRefInExpr($item, $callStmt, $pushedVars);
}
}
}
}
}

@ -30,6 +30,7 @@ use PhpAot\Php\Generator\Utils;
use PhpAot\Php\Generator\TypeCheckGenerator;
use PhpAot\Php\Optimizer\SsaPropOptimizer;
use PhpAot\Php\Optimizer\SsaTypeOptimizer;
use PhpAot\Php\Optimizer\LoopVarOptimizer;
use PhpAot\Php\Parser\StdContainerTrait;
use PhpAot\Php\Parser\AssignOpTrait;
use PhpAot\Php\Parser\BinaryOpTrait;
@ -76,6 +77,7 @@ class CompilerBase extends \PhpAot\Core\Translator
use Utils;
use TypeCheckGenerator;
use SsaTypeOptimizer;
use LoopVarOptimizer;
use SsaPropOptimizer;
public const string TYPE_VAR = 'php::Var';
@ -5288,4 +5290,4 @@ class CompilerBase extends \PhpAot\Core\Translator
return $tmpVar;
}
}
}

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

@ -284,32 +284,52 @@ trait SsaPropOptimizer
}
}
// Check function/method call arguments for &$o->prop patterns
if ($stmt instanceof Node\Stmt\Expression) {
$expr = $stmt->expr;
$args = null;
if ($expr instanceof Expr\FuncCall) {
$args = $expr->args;
} elseif ($expr instanceof Expr\MethodCall || $expr instanceof Expr\StaticCall
|| $expr instanceof Expr\NullsafeMethodCall) {
$args = $expr->args;
}
if ($args) {
foreach ($args as $arg) {
// Explicit &$o->prop
if ($arg->byRef && $this->isPropOfObj($arg->value, $objName)) {
if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) {
return true;
}
}
if (($stmt instanceof Node\Stmt\If_
|| $stmt instanceof Node\Stmt\While_
|| $stmt instanceof Node\Stmt\Do_)
&& $stmt->cond instanceof Node
&& $this->exprHasDangerousPropOp($stmt->cond, $objName)) {
return true;
}
if ($stmt instanceof Node\Stmt\For_) {
foreach ([$stmt->init, $stmt->cond, $stmt->loop] as $exprList) {
foreach ($exprList as $expr) {
if ($expr instanceof Node && $this->exprHasDangerousPropOp($expr, $objName)) {
return true;
}
// refval($o->prop) pseudo-function
if ($arg->value instanceof Expr\FuncCall
&& $arg->value->name instanceof Node\Name
&& $arg->value->name->toLowerString() === 'refval'
&& !empty($arg->value->args)) {
$inner = $arg->value->args[0]->value;
if ($this->isPropOfObj($inner, $objName)) {
return true;
}
}
}
}
}
if ($stmt instanceof Node\Stmt\Foreach_ && $stmt->expr instanceof Node) {
if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Switch_ && $stmt->cond instanceof Node) {
if ($this->exprHasDangerousPropOp($stmt->cond, $objName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr instanceof Node) {
if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Echo_) {
foreach ($stmt->exprs as $expr) {
if ($expr instanceof Node && $this->exprHasDangerousPropOp($expr, $objName)) {
return true;
}
}
}
@ -318,6 +338,66 @@ trait SsaPropOptimizer
return $this->recurseDangerousPropOp($stmt, $objName);
}
protected function exprHasDangerousPropOp($expr, string $objName): bool
{
if (!$expr instanceof Node) {
return false;
}
if ($expr instanceof Expr\AssignRef && $this->isPropOfObj($expr->expr, $objName)) {
return true;
}
if ($expr instanceof Expr\FuncCall
&& $expr->name instanceof Node\Name
&& $expr->name->toLowerString() === 'refval'
&& !empty($expr->args)
&& $this->isPropOfObj($expr->args[0]->value, $objName)) {
return true;
}
if ($expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall
|| $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) {
foreach ($expr->args as $arg) {
if ($arg->byRef && $this->isPropOfObj($arg->value, $objName)) {
return true;
}
if ($this->exprHasDangerousPropOp($arg->value, $objName)) {
return true;
}
}
if (($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall)
&& $this->exprHasDangerousPropOp($expr->var, $objName)) {
return true;
}
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr
&& $this->exprHasDangerousPropOp($expr->class, $objName)) {
return true;
}
return false;
}
foreach (['left', 'right', 'expr', 'var', 'cond', 'if', 'else', 'dim', 'value'] as $prop) {
if (isset($expr->$prop) && $expr->$prop instanceof Node) {
if ($this->exprHasDangerousPropOp($expr->$prop, $objName)) {
return true;
}
}
}
foreach (['args', 'exprs', 'items'] as $prop) {
if (isset($expr->$prop) && is_array($expr->$prop)) {
foreach ($expr->$prop as $item) {
if ($item instanceof Node && $this->exprHasDangerousPropOp($item, $objName)) {
return true;
}
}
}
}
return false;
}
/**
* Check if an expression is a property fetch on a specific object.
*/

@ -18,8 +18,8 @@ use PhpParser\NodeAbstract;
trait SsaTypeOptimizer
{
/**
* Binary ops that always produce int from int operands and never overflow
* to float. Bitwise operations and modulo are safe; arithmetic is not.
* Binary ops that are safe while reading a narrowed int. Arithmetic is
* excluded because it may overflow to float in PHP.
*/
protected const array SAFE_INT_BINARY_OPS = [
'Expr_BinaryOp_BitwiseAnd' => true,
@ -28,12 +28,33 @@ trait SsaTypeOptimizer
'Expr_BinaryOp_ShiftLeft' => true,
'Expr_BinaryOp_ShiftRight' => true,
'Expr_BinaryOp_Mod' => true,
'Expr_BinaryOp_Equal' => true,
'Expr_BinaryOp_NotEqual' => true,
'Expr_BinaryOp_Identical' => true,
'Expr_BinaryOp_NotIdentical' => true,
'Expr_BinaryOp_Greater' => true,
'Expr_BinaryOp_GreaterOrEqual' => true,
'Expr_BinaryOp_Smaller' => true,
'Expr_BinaryOp_SmallerOrEqual' => true,
'Expr_BinaryOp_Spaceship' => true,
'Expr_BinaryOp_BooleanAnd' => true,
'Expr_BinaryOp_BooleanOr' => true,
'Expr_BinaryOp_LogicalAnd' => true,
'Expr_BinaryOp_LogicalOr' => true,
'Expr_BinaryOp_Coalesce' => true,
];
/**
* Compound assignment ops that keep the variable int-typed.
* Ops that force numeric operands through integer-only PHP semantics.
* They are unsafe for variables narrowed to php::Float.
*/
protected const array SAFE_INT_ASSIGN_OPS = [
protected const array FLOAT_INT_ONLY_OPS = [
'Expr_BinaryOp_BitwiseAnd' => true,
'Expr_BinaryOp_BitwiseOr' => true,
'Expr_BinaryOp_BitwiseXor' => true,
'Expr_BinaryOp_ShiftLeft' => true,
'Expr_BinaryOp_ShiftRight' => true,
'Expr_BinaryOp_Mod' => true,
'Expr_AssignOp_BitwiseAnd' => true,
'Expr_AssignOp_BitwiseOr' => true,
'Expr_AssignOp_BitwiseXor' => true,
@ -179,12 +200,22 @@ trait SsaTypeOptimizer
if ($def instanceof Node\Stmt\Expression && $def->expr instanceof Node\Expr\Assign) {
$expr = $def->expr->expr;
$type = $this->detectTypeOfExpr($expr);
if ($type === self::TYPE_INT && $this->exprCanOverflowInt($expr)) {
if ($type === self::TYPE_INT && !$this->isSafeSsaIntExpr($expr)) {
return null;
}
return $type;
}
if ($def instanceof Node\Stmt\Expression && $def->expr instanceof Node\Expr\AssignOp) {
return $this->detectAssignOpDefType($def->expr);
}
if ($def instanceof Node\Stmt\Expression
&& ($def->expr instanceof Node\Expr\PreInc || $def->expr instanceof Node\Expr\PostInc
|| $def->expr instanceof Node\Expr\PreDec || $def->expr instanceof Node\Expr\PostDec)) {
return null;
}
if ($def instanceof Node\Stmt\Foreach_) {
return null;
}
@ -205,6 +236,69 @@ trait SsaTypeOptimizer
return null;
}
protected function detectAssignOpDefType(Node\Expr\AssignOp $expr): ?string
{
if ($expr instanceof Node\Expr\AssignOp\Div) {
return self::TYPE_FLOAT;
}
if ($expr instanceof Node\Expr\AssignOp\Concat) {
return self::TYPE_STR;
}
if ($expr instanceof Node\Expr\AssignOp\Pow) {
return null;
}
if ($expr instanceof Node\Expr\AssignOp\Plus
|| $expr instanceof Node\Expr\AssignOp\Minus
|| $expr instanceof Node\Expr\AssignOp\Mul) {
$rhsType = $this->detectTypeOfExpr($expr->expr);
if ($rhsType === self::TYPE_FLOAT) {
return self::TYPE_FLOAT;
}
if ($rhsType === self::TYPE_INT) {
return null;
}
return $rhsType;
}
return null;
}
/**
* Return true only for int-producing expressions whose C++ native-int
* evaluation matches PHP without relying on range analysis.
*/
protected function isSafeSsaIntExpr(NodeAbstract $expr): bool
{
if ($expr instanceof Node\Scalar\LNumber) {
return true;
}
if ($expr instanceof Node\Expr\Cast\Int_) {
return true;
}
if ($expr instanceof Node\Expr\ConstFetch) {
return $this->detectConstType($expr) === self::TYPE_INT;
}
if ($expr instanceof Node\Expr\BitwiseNot) {
return $this->isSafeSsaIntExpr($expr->expr);
}
if ($expr instanceof Node\Expr\BinaryOp\BitwiseAnd
|| $expr instanceof Node\Expr\BinaryOp\BitwiseOr
|| $expr instanceof Node\Expr\BinaryOp\BitwiseXor
|| $expr instanceof Node\Expr\BinaryOp\Mod) {
return $this->isSafeSsaIntExpr($expr->left)
&& $this->isSafeSsaIntExpr($expr->right);
}
return false;
}
/**
* Check whether a TYPE_INT expression could overflow int64 at runtime
* and produce a float in PHP.
@ -219,24 +313,7 @@ trait SsaTypeOptimizer
if ($expr instanceof Node\Expr\BinaryOp\Plus
|| $expr instanceof Node\Expr\BinaryOp\Minus
|| $expr instanceof Node\Expr\BinaryOp\Mul) {
$leftConst = $this->getIntConstantValue($expr->left);
$rightConst = $this->getIntConstantValue($expr->right);
if ($leftConst !== null && $rightConst !== null) {
$result = match (true) {
$expr instanceof Node\Expr\BinaryOp\Plus => $leftConst + $rightConst,
$expr instanceof Node\Expr\BinaryOp\Minus => $leftConst - $rightConst,
$expr instanceof Node\Expr\BinaryOp\Mul => $leftConst * $rightConst,
};
return $result > PHP_INT_MAX || $result < PHP_INT_MIN;
}
if ($this->isBoundaryConstant($expr->left)
|| $this->isBoundaryConstant($expr->right)) {
return true;
}
return false;
return true;
}
// ** can easily overflow (e.g. 2**63 already exceeds INT64_MAX)
@ -258,14 +335,6 @@ trait SsaTypeOptimizer
return false;
}
protected function getIntConstantValue(NodeAbstract $node): ?int
{
if ($node instanceof Node\Scalar\LNumber) {
return $node->value;
}
return null;
}
protected function isBoundaryConstant(NodeAbstract $node): bool
{
if ($node instanceof Node\Expr\ConstFetch
@ -408,12 +477,9 @@ trait SsaTypeOptimizer
|| $this->exprHasIntHazard($expr->right, $varName);
}
// Safe compound assignment: $varName &= ..., etc. — recurse into RHS
if (isset(self::SAFE_INT_ASSIGN_OPS[$type])) {
return $this->exprHasIntHazard($expr->expr, $varName);
}
// Unsafe compound assignment: $varName += ..., etc.
// Compound assignment can change the variable type or depends on
// PHP's integer conversion edge cases. Treat all of them as hazards
// for int narrowing.
if ($expr instanceof Node\Expr\AssignOp) {
if ($this->isVarNamed($expr->var, $varName)) {
return true;
@ -533,15 +599,49 @@ trait SsaTypeOptimizer
return false;
}
if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\AssignOp) {
$lhs = $stmt->expr->var;
if ($lhs instanceof Expr\Variable && is_string($lhs->name) && $lhs->name === $varName) {
if ($stmt->expr instanceof Expr\AssignOp\BitwiseAnd
|| $stmt->expr instanceof Expr\AssignOp\BitwiseOr
|| $stmt->expr instanceof Expr\AssignOp\BitwiseXor
|| $stmt->expr instanceof Expr\AssignOp\ShiftLeft
|| $stmt->expr instanceof Expr\AssignOp\ShiftRight
|| $stmt->expr instanceof Expr\AssignOp\Mod) {
if ($stmt instanceof Node\Stmt\Expression && $this->exprHasFloatHazard($stmt->expr, $varName)) {
return true;
}
if (($stmt instanceof Node\Stmt\If_
|| $stmt instanceof Node\Stmt\While_
|| $stmt instanceof Node\Stmt\Do_)
&& $stmt->cond instanceof Node
&& $this->exprHasFloatHazard($stmt->cond, $varName)) {
return true;
}
if ($stmt instanceof Node\Stmt\For_) {
foreach ([$stmt->init, $stmt->cond, $stmt->loop] as $exprList) {
foreach ($exprList as $expr) {
if ($expr instanceof Node && $this->exprHasFloatHazard($expr, $varName)) {
return true;
}
}
}
}
if ($stmt instanceof Node\Stmt\Foreach_ && $stmt->expr instanceof Node) {
if ($this->exprHasFloatHazard($stmt->expr, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Switch_ && $stmt->cond instanceof Node) {
if ($this->exprHasFloatHazard($stmt->cond, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr instanceof Node) {
if ($this->exprHasFloatHazard($stmt->expr, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Echo_) {
foreach ($stmt->exprs as $expr) {
if ($expr instanceof Node && $this->exprHasFloatHazard($expr, $varName)) {
return true;
}
}
@ -550,6 +650,37 @@ trait SsaTypeOptimizer
return $this->recurseForDangerousOps($stmt, $varName, 'Float');
}
protected function exprHasFloatHazard($expr, string $varName): bool
{
if (!$expr instanceof Node) {
return false;
}
if (isset(self::FLOAT_INT_ONLY_OPS[$expr->getType()]) && $this->exprUsesVar($expr, $varName)) {
return true;
}
foreach (['left', 'right', 'expr', 'var', 'cond', 'if', 'else', 'dim', 'value'] as $prop) {
if (isset($expr->$prop) && $expr->$prop instanceof Node) {
if ($this->exprHasFloatHazard($expr->$prop, $varName)) {
return true;
}
}
}
foreach (['args', 'exprs', 'items', 'stmts'] as $prop) {
if (isset($expr->$prop) && is_array($expr->$prop)) {
foreach ($expr->$prop as $item) {
if ($item instanceof Node && $this->exprHasFloatHazard($item, $varName)) {
return true;
}
}
}
}
return false;
}
/**
* Recurse into compound statements (if/else, foreach, while, for, try/catch, switch).
*/

@ -2398,6 +2398,8 @@ CODE;
$this->context->ssaBuilder->build();
// Narrow local variable types based on SSA analysis
$this->optimizeVarTypes();
// Narrow range-proven loop counters independent of native_types
$this->optimizeLoopVars();
// Analyze object stability for property reference hoisting
$this->optimizeObjectProps();
}

@ -13,7 +13,8 @@ function main(): void {
$b = 99;
var_dump($b);
// Int compound assigns, all with int RHS → narrowed to int
// Int compound assigns still produce the expected runtime value,
// but SSA no longer uses them to prove native int narrowing.
$c = 10;
$c += 5;
$c *= 3;

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

@ -13,7 +13,7 @@ function main(): void {
$c = $a * PHP_INT_MAX;
echo gettype($c), PHP_EOL;
// Two small ints added safely → narrowed to int
// Even small int arithmetic must keep PHP overflow semantics.
$d = $a + 99;
var_dump($d);
}

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

@ -13,7 +13,8 @@ function main(): void {
$b **= 3;
var_dump($b);
// *= with int RHS is safe → narrowed to int
// *= with int RHS produces the expected runtime value,
// but is not used as proof for native int narrowing.
$c = 5;
$c *= 4;
var_dump($c);

@ -8,7 +8,7 @@ function main(): void {
$a = 200;
echo $b, PHP_EOL;
// $c not referenced → can be narrowed
// $c is not referenced; arithmetic still keeps PHP runtime semantics.
$c = 50;
$c += 25;
echo $c, PHP_EOL;

@ -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…
Cancel
Save