diff --git a/phpunit/src/Analysis/SsaBuilderTest.php b/phpunit/src/Analysis/SsaBuilderTest.php index 1b5045a7..caf61761 100644 --- a/phpunit/src/Analysis/SsaBuilderTest.php +++ b/phpunit/src/Analysis/SsaBuilderTest.php @@ -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(' diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index 043205b8..d0601c9d 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -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('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'); diff --git a/src/Php/Analysis/SsaBuilder.php b/src/Php/Analysis/SsaBuilder.php index 102b070a..852d4cdf 100644 --- a/src/Php/Analysis/SsaBuilder.php +++ b/src/Php/Analysis/SsaBuilder.php @@ -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); + } + } + } } } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index a57c2fa2..a1b4b80f 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -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; } -} \ No newline at end of file +} diff --git a/src/Php/Optimizer/LoopVarOptimizer.php b/src/Php/Optimizer/LoopVarOptimizer.php new file mode 100644 index 00000000..9b993e01 --- /dev/null +++ b/src/Php/Optimizer/LoopVarOptimizer.php @@ -0,0 +1,734 @@ + 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, deps?: array}> $candidates + */ + protected function allowLoopNode(array &$candidates, string $varName, int $nodeId): void + { + $candidates[$varName]['allowed'][$nodeId] = true; + } + + /** + * @param array, deps?: array}> $candidates + */ + protected function requireLoopVar(array &$candidates, string $varName, string $depName): void + { + if ($varName !== $depName) { + $candidates[$varName]['deps'][$depName] = true; + } + } + + /** + * @param array $safeVars + * @param array, deps?: array}> $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 $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 $safeVars + * @param array, deps?: array}> $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 $safeVars + * @param array, deps?: array}> $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 $safeVars + * @return array{vars: array, intVars: array}|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 $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 + */ + protected function collectLoopExprVars(NodeAbstract $expr): array + { + $vars = []; + $this->collectLoopExprVarsInto($expr, $vars); + return $vars; + } + + /** + * @param array $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 $safeVars + * @return array + */ + 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 $vars + */ + protected function loopBodyMutatesAny(array $stmts, array $vars): bool + { + foreach ($stmts as $stmt) { + if ($this->loopNodeMutatesAny($stmt, $vars)) { + return true; + } + } + return false; + } + + /** + * @param array $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 $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 $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 $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 $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 $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; + } +} diff --git a/src/Php/Optimizer/SsaPropOptimizer.php b/src/Php/Optimizer/SsaPropOptimizer.php index f0eb12a6..e333cfa7 100644 --- a/src/Php/Optimizer/SsaPropOptimizer.php +++ b/src/Php/Optimizer/SsaPropOptimizer.php @@ -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. */ diff --git a/src/Php/Optimizer/SsaTypeOptimizer.php b/src/Php/Optimizer/SsaTypeOptimizer.php index b0e40446..bcea8107 100644 --- a/src/Php/Optimizer/SsaTypeOptimizer.php +++ b/src/Php/Optimizer/SsaTypeOptimizer.php @@ -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). */ diff --git a/src/Php/Translator.php b/src/Php/Translator.php index b16e6911..da3b3fb6 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -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(); } diff --git a/tests/aot/optimizations/float-narrow-mixed.phpt b/tests/aot/optimizations/float-narrow-mixed.phpt index 77f24541..2cff5418 100644 --- a/tests/aot/optimizations/float-narrow-mixed.phpt +++ b/tests/aot/optimizations/float-narrow-mixed.phpt @@ -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; diff --git a/tests/aot/optimizations/float-narrow-nested-int-ops.phpt b/tests/aot/optimizations/float-narrow-nested-int-ops.phpt new file mode 100644 index 00000000..1dbd6750 --- /dev/null +++ b/tests/aot/optimizations/float-narrow-nested-int-ops.phpt @@ -0,0 +1,16 @@ +--TEST-- +SSA narrowing: nested integer-only ops on float prevent narrowing +--FILE-- + +--EXPECT-- +int(2) +int(2) diff --git a/tests/aot/optimizations/int-narrow-overflow.phpt b/tests/aot/optimizations/int-narrow-overflow.phpt index c155ec47..f04191dc 100644 --- a/tests/aot/optimizations/int-narrow-overflow.phpt +++ b/tests/aot/optimizations/int-narrow-overflow.phpt @@ -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); } diff --git a/tests/aot/optimizations/loop-var-for-const-bound.phpt b/tests/aot/optimizations/loop-var-for-const-bound.phpt new file mode 100644 index 00000000..6fec3bda --- /dev/null +++ b/tests/aot/optimizations/loop-var-for-const-bound.phpt @@ -0,0 +1,23 @@ +--TEST-- +Loop var optimizer: for counter with constant bound variable +--FILE-- + +--EXPECT-- +int(20000) +int(20000) +int(2) diff --git a/tests/aot/optimizations/loop-var-for-int-function-bound.phpt b/tests/aot/optimizations/loop-var-for-int-function-bound.phpt new file mode 100644 index 00000000..4819154f --- /dev/null +++ b/tests/aot/optimizations/loop-var-for-int-function-bound.phpt @@ -0,0 +1,22 @@ +--TEST-- +Loop var optimizer: for counter with typed int function bound +--FILE-- + +--EXPECT-- +int(5) +int(4) diff --git a/tests/aot/optimizations/loop-var-for-int-function-desc.phpt b/tests/aot/optimizations/loop-var-for-int-function-desc.phpt new file mode 100644 index 00000000..dde42fc5 --- /dev/null +++ b/tests/aot/optimizations/loop-var-for-int-function-desc.phpt @@ -0,0 +1,22 @@ +--TEST-- +Loop var optimizer: descending for counter with typed int function init +--FILE-- + 0; $i--) { + $sum += $i; + } + + var_dump($i); + var_dump($sum); +} +?> +--EXPECT-- +int(0) +int(10) diff --git a/tests/aot/optimizations/loop-var-for-strlen-bound.phpt b/tests/aot/optimizations/loop-var-for-strlen-bound.phpt new file mode 100644 index 00000000..9d173535 --- /dev/null +++ b/tests/aot/optimizations/loop-var-for-strlen-bound.phpt @@ -0,0 +1,19 @@ +--TEST-- +Loop var optimizer: for counter with strlen bound +--FILE-- + +--EXPECT-- +int(7) +int(6) diff --git a/tests/aot/optimizations/loop-var-while-postdec.phpt b/tests/aot/optimizations/loop-var-while-postdec.phpt new file mode 100644 index 00000000..f3005e6c --- /dev/null +++ b/tests/aot/optimizations/loop-var-while-postdec.phpt @@ -0,0 +1,21 @@ +--TEST-- +Loop var optimizer: while post-decrement from constant +--FILE-- + +--EXPECT-- +int(-1) +int(3) diff --git a/tests/aot/optimizations/narrow-div-pow.phpt b/tests/aot/optimizations/narrow-div-pow.phpt index 0568513c..2155915f 100644 --- a/tests/aot/optimizations/narrow-div-pow.phpt +++ b/tests/aot/optimizations/narrow-div-pow.phpt @@ -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); diff --git a/tests/aot/optimizations/narrow-ref-prevent.phpt b/tests/aot/optimizations/narrow-ref-prevent.phpt index 45b7b721..f7a0b483 100644 --- a/tests/aot/optimizations/narrow-ref-prevent.phpt +++ b/tests/aot/optimizations/narrow-ref-prevent.phpt @@ -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; diff --git a/tests/aot/optimizations/objprop-hoist-loop-redef.phpt b/tests/aot/optimizations/objprop-hoist-loop-redef.phpt new file mode 100644 index 00000000..2acadb58 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-loop-redef.phpt @@ -0,0 +1,30 @@ +--TEST-- +SSA object prop: loop body object redefinition prevents hoisting +--FILE-- +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) diff --git a/tests/aot/optimizations/objprop-hoist-nested-refval.phpt b/tests/aot/optimizations/objprop-hoist-nested-refval.phpt new file mode 100644 index 00000000..9136af26 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-nested-refval.phpt @@ -0,0 +1,27 @@ +--TEST-- +SSA object prop: nested refval property use prevents hoisting +--FILE-- +a = 10; + + $ignored = mutate(refval($o->a)); + $o->a += 5; + + var_dump($o->a); +} +?> +--EXPECT-- +int(25)