From bf31da8e7b107489ecc2c08dc2faff69d01eb778 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 4 Jun 2026 21:13:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(compiler):=20=E5=AE=9E=E7=8E=B0SSA?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=92=8C=E7=B1=BB=E5=9E=8B=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加SsaBuilder类用于构建SSA形式和e-SSA分析 - 引入SsaTypeOptimizer trait实现基于SSA的类型窄化优化 - 将FuncCallOptimizer移至Optimizer命名空间 - 在FunctionContext中添加SSA分析器实例 - 实现变量类型的自动窄化为C++原生类型(int/float) - 添加防止危险操作的类型安全检查 - 支持常量折叠和溢出检测逻辑 - 重构函数参数名称以提高代码可读性 - 添加完整的SSA构建器单元测试覆盖各种场景 --- phpunit/src/Analysis/SsaBuilderTest.php | 472 +++++ src/Php/Analysis/SsaBuilder.php | 1694 +++++++++++++++++ src/Php/CompilerBase.php | 16 +- src/Php/Context/FunctionContext.php | 5 + src/Php/{ => Optimizer}/FuncCallOptimizer.php | 2 +- src/Php/Optimizer/SsaTypeOptimizer.php | 353 ++++ .../aot/optimizations/float-bitwise-mod.phpt | 26 + .../aot/optimizations/float-narrow-basic.phpt | 26 + .../aot/optimizations/float-narrow-mixed.phpt | 26 + tests/aot/optimizations/int-calculation.phpt | 18 + tests/aot/optimizations/int-narrow-basic.phpt | 25 + .../optimizations/int-narrow-overflow.phpt | 24 + tests/aot/optimizations/narrow-div-pow.phpt | 25 + .../aot/optimizations/narrow-ref-prevent.phpt | 19 + 14 files changed, 2728 insertions(+), 3 deletions(-) create mode 100644 phpunit/src/Analysis/SsaBuilderTest.php create mode 100644 src/Php/Analysis/SsaBuilder.php rename src/Php/{ => Optimizer}/FuncCallOptimizer.php (99%) create mode 100644 src/Php/Optimizer/SsaTypeOptimizer.php create mode 100644 tests/aot/optimizations/float-bitwise-mod.phpt create mode 100644 tests/aot/optimizations/float-narrow-basic.phpt create mode 100644 tests/aot/optimizations/float-narrow-mixed.phpt create mode 100644 tests/aot/optimizations/int-calculation.phpt create mode 100644 tests/aot/optimizations/int-narrow-basic.phpt create mode 100644 tests/aot/optimizations/int-narrow-overflow.phpt create mode 100644 tests/aot/optimizations/narrow-div-pow.phpt create mode 100644 tests/aot/optimizations/narrow-ref-prevent.phpt diff --git a/phpunit/src/Analysis/SsaBuilderTest.php b/phpunit/src/Analysis/SsaBuilderTest.php new file mode 100644 index 00000000..1b5045a7 --- /dev/null +++ b/phpunit/src/Analysis/SsaBuilderTest.php @@ -0,0 +1,472 @@ +createForHostVersion(); + $stmts = $parser->parse('assertNotNull($stmts); + return $stmts; + } + + private function getFunctionStmts(string $code): array + { + $stmts = $this->parsePhp($code); + $fn = $stmts[0]; + if ($fn instanceof Stmt\Expression) { + // For standalone expressions, wrap in a function-like context + return $stmts; + } + $this->assertInstanceOf(FunctionLike::class, $fn); + return $fn->getStmts() ?: []; + } + + private function buildSsa(string $code): SsaBuilder + { + $stmts = $this->parsePhp('function f($x) { ' . $code . ' }'); + $fn = $stmts[0]; + $params = []; + foreach ($fn->getParams() as $param) { + $params[] = (object)[ + 'name' => $param->var->name, + 'byRef' => $param->byRef ? 1 : 0, + 'variadic' => $param->variadic, + ]; + } + $builder = new SsaBuilder($fn->getStmts() ?: [], $params); + $builder->build(); + return $builder; + } + + // ======================================================================== + // Basic block construction + // ======================================================================== + + public function testSingleAssignmentSingleBlock(): void + { + $builder = $this->buildSsa('$x = 1;'); + $this->assertCount(2, $builder->blocks); // entry + exit + $this->assertEquals(1, count($builder->blocks[0]->stmts)); + $this->assertCount(0, $builder->blocks[0]->predecessors); + $this->assertContains($builder->blocks[count($builder->blocks) - 1]->id, $builder->blocks[0]->successors); + } + + public function testMultipleStatementsSingleBlock(): void + { + $builder = $this->buildSsa('$a = 1; $b = 2; $c = $a + $b;'); + $this->assertCount(2, $builder->blocks); + $this->assertCount(3, $builder->blocks[0]->stmts); + } + + public function testReturnSplitsBlock(): void + { + $builder = $this->buildSsa('$a = 1; return $a; $b = 2;'); + // Should have 3 blocks: entry (a=1, return), after return (b=2), exit + $this->assertGreaterThanOrEqual(3, count($builder->blocks)); + } + + // ======================================================================== + // Goto and label handling + // ======================================================================== + + public function testGotoSplitsBlocks(): void + { + $builder = $this->buildSsa(' + goto end; + $a = 1; + end: + $b = 2; + '); + + $this->assertGreaterThanOrEqual(3, count($builder->blocks)); + + // Find the goto block + $gotoBlock = null; + $labelBlock = null; + foreach ($builder->blocks as $block) { + if ($block->endsWithGoto && $block->gotoLabel === 'end') { + $gotoBlock = $block; + } + if ($block->isGotoTarget && $block->labelName === 'end') { + $labelBlock = $block; + } + } + $this->assertNotNull($gotoBlock, 'Goto block not found'); + $this->assertNotNull($labelBlock, 'Label block not found'); + $this->assertTrue($gotoBlock->endsWithGoto); + $this->assertContains($labelBlock->id, $gotoBlock->successors); + $this->assertContains($gotoBlock->id, $labelBlock->predecessors); + } + + public function testLabelBlockMapping(): void + { + $builder = $this->buildSsa(' + goto skip; + skip: + $x = 1; + '); + $labelBlockId = $builder->getLabelBlock('skip'); + $this->assertNotNull($labelBlockId); + $this->assertTrue($builder->blocks[$labelBlockId]->isGotoTarget); + } + + // ======================================================================== + // SSA variable renaming + // ======================================================================== + + public function testParameterCreatesSsaVar(): void + { + $builder = $this->buildSsa('$y = $x;'); + $this->assertNotEmpty($builder->ssaVars); + + $paramVar = null; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'x' && ($var->flags & SsaFlags::PARAM)) { + $paramVar = $var; + break; + } + } + $this->assertNotNull($paramVar, 'Parameter x should have an SSA var'); + } + + public function testAssignmentCreatesNewSsaVar(): void + { + $builder = $this->buildSsa('$x = 1; $x = 2;'); + $xVars = []; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'x' && !($var->flags & SsaFlags::PARAM) && !($var->flags & SsaFlags::PHI)) { + $xVars[] = $var; + } + } + $this->assertCount(2, $xVars, 'Two assignments to $x should create two SSA vars'); + } + + public function testVarDefBlocks(): void + { + $builder = $this->buildSsa('$a = 1; $b = 2;'); + $aBlocks = $builder->getDefBlocks('a'); + $bBlocks = $builder->getDefBlocks('b'); + $this->assertNotEmpty($aBlocks); + $this->assertNotEmpty($bBlocks); + } + + // ======================================================================== + // Branching and φ function placement + // ======================================================================== + + public function testIfBranchCreatesMultipleBlocks(): void + { + $builder = $this->buildSsa(' + $x = 1; + if ($x > 0) { + $x = 2; + } + return $x; + '); + // Should have more than just entry + exit + $this->assertGreaterThan(2, count($builder->blocks)); + } + + public function testDominatorTreeComputed(): void + { + $builder = $this->buildSsa(' + $x = 1; + if ($x > 0) { + $x = 2; + } else { + $x = 3; + } + return $x; + '); + // Entry block dominates all others + $entryId = 0; + foreach ($builder->blocks as $block) { + if ($block->id === $entryId) continue; + $this->assertGreaterThanOrEqual(0, $block->dominator, "Block {$block->id} should have a dominator"); + } + } + + public function testPhiFunctionPlacedAtJoin(): void + { + $builder = $this->buildSsa(' + $x = 1; + if ($x > 0) { + $x = 2; + } else { + $x = 3; + } + return $x; + '); + + // Check for φ function for $x at the join point + $hasPhi = false; + foreach ($builder->blocks as $block) { + if ($builder->hasPhiAtBlock('x', $block->id)) { + $hasPhi = true; + break; + } + } + $this->assertTrue($hasPhi, 'φ function should be placed at join point for $x'); + } + + // ======================================================================== + // unset() handling + // ======================================================================== + + public function testUnsetKillsVariable(): void + { + $builder = $this->buildSsa(' + $x = 1; + unset($x); + $x = 2; + '); + + $killedVar = null; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'x' && ($var->flags & SsaFlags::KILLED)) { + $killedVar = $var; + break; + } + } + $this->assertNotNull($killedVar, 'unset($x) should create a KILLED SSA var'); + $this->assertTrue((bool)($killedVar->flags & SsaFlags::UNDEFINED)); + } + + // ======================================================================== + // Reference handling + // ======================================================================== + + public function testAssignRefCreatesRefSsaVar(): void + { + $builder = $this->buildSsa(' + $a = 1; + $b =& $a; + '); + + $refVar = null; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'b' && ($var->flags & SsaFlags::REFERENCE)) { + $refVar = $var; + break; + } + } + $this->assertNotNull($refVar, 'AssignRef should create REFERENCE SSA var'); + } + + public function testCallByRefCreatesEscapedVar(): void + { + $builder = $this->buildSsa(' + foo(&$x); + '); + + $escapedVar = null; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'x' && ($var->flags & SsaFlags::ESCAPED)) { + $escapedVar = $var; + break; + } + } + $this->assertNotNull($escapedVar, 'Call by reference should create ESCAPED SSA var'); + } + + public function testRefvalCallByRefCreatesEscapedVar(): void + { + // refval() is the AOT compiler's pseudo-function for dynamic call reference passing + $builder = $this->buildSsa(' + 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() call should create ESCAPED SSA var for its argument'); + } + + public function testRefvalWithMultipleArgs(): void + { + $builder = $this->buildSsa(' + some_func($a, refval($b), refval($c)); + '); + + $escapedB = false; + $escapedC = false; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'b' && ($var->flags & SsaFlags::ESCAPED)) { + $escapedB = true; + } + if ($var->origName === 'c' && ($var->flags & SsaFlags::ESCAPED)) { + $escapedC = true; + } + } + $this->assertTrue($escapedB, 'refval($b) should create ESCAPED SSA var'); + $this->assertTrue($escapedC, 'refval($c) should create ESCAPED SSA var'); + + // $a is NOT passed by ref — it should NOT be escaped + $aEscaped = false; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'a' && ($var->flags & SsaFlags::ESCAPED)) { + $aEscaped = true; + } + } + $this->assertFalse($aEscaped, '$a (not refval) should NOT be escaped'); + } + + // ======================================================================== + // e-SSA pi constraints + // ======================================================================== + + public function testPiConstraintForInstanceof(): void + { + $builder = $this->buildSsa(' + if ($x instanceof Foo) { + $y = $x; + } + '); + + // Find the if statement in the first block + $ifStmt = null; + foreach ($builder->blocks[0]->stmts as $stmt) { + if ($stmt instanceof Stmt\If_) { + $ifStmt = $stmt; + break; + } + } + $this->assertNotNull($ifStmt, 'If statement should be in first block'); + + $result = $builder->buildPiConstraints($ifStmt); + $this->assertNotEmpty($result['trueVars']); + $this->assertArrayHasKey('x', $result['trueVars']); + $this->assertEquals('Foo', $result['trueVars']['x']->narrowedType); + } + + public function testPiConstraintForIsInt(): void + { + $stmts = $this->parsePhp('function f($x) { if (is_int($x)) { return $x; } }'); + $fn = $stmts[0]; + $builder = new SsaBuilder($fn->getStmts() ?: [], []); + $builder->build(); + + $ifStmt = $builder->blocks[0]->stmts[0] ?? null; + $this->assertInstanceOf(Stmt\If_::class, $ifStmt); + + $result = $builder->buildPiConstraints($ifStmt); + if (isset($result['trueVars']['x'])) { + $this->assertEquals('int', $result['trueVars']['x']->narrowedType); + } + } + + public function testPiConstraintNegation(): void + { + $stmts = $this->parsePhp('function f($x) { if (!$x instanceof Foo) { return; } }'); + $fn = $stmts[0]; + $builder = new SsaBuilder($fn->getStmts() ?: [], []); + $builder->build(); + + $ifStmt = $builder->blocks[0]->stmts[0] ?? null; + $this->assertInstanceOf(Stmt\If_::class, $ifStmt); + + $result = $builder->buildPiConstraints($ifStmt); + // Negation flips true/false — the TRUE branch should have NOT Foo + if (isset($result['trueVars']['x'])) { + $this->assertStringContainsString('!', $result['trueVars']['x']->narrowedType); + } + if (isset($result['falseVars']['x'])) { + $this->assertStringNotContainsString('!', $result['falseVars']['x']->narrowedType); + } + } + + // ======================================================================== + // Dump output + // ======================================================================== + + public function testDumpProducesOutput(): void + { + $builder = $this->buildSsa('$x = 1; $y = $x;'); + $dump = $builder->dump(); + $this->assertStringContainsString('SSA Builder Dump', $dump); + $this->assertStringContainsString('Blocks:', $dump); + $this->assertStringContainsString('SSA Vars:', $dump); + } + + // ======================================================================== + // Edge cases + // ======================================================================== + + public function testEmptyFunction(): void + { + $builder = $this->buildSsa(''); + $this->assertCount(2, $builder->blocks); // entry + exit + } + + public function testForeachDefinesVariables(): void + { + $builder = $this->buildSsa(' + foreach ($arr as $key => $value) { + $sum = $sum + $value; + } + '); + + $hasKey = false; + $hasValue = false; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'key') $hasKey = true; + if ($var->origName === 'value') $hasValue = true; + } + $this->assertTrue($hasKey, 'foreach key variable should be defined'); + $this->assertTrue($hasValue, 'foreach value variable should be defined'); + } + + public function testStaticVariableIsEscaped(): void + { + $builder = $this->buildSsa(' + static $count = 0; + $count++; + '); + + $staticVar = null; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'count' && ($var->flags & SsaFlags::ESCAPED)) { + $staticVar = $var; + break; + } + } + $this->assertNotNull($staticVar, 'static variable should have ESCAPED flag'); + } + + public function testCatchVariable(): void + { + $builder = $this->buildSsa(' + try { + throw new Exception(); + } catch (Exception $e) { + $msg = $e->getMessage(); + } + '); + + $hasE = false; + foreach ($builder->ssaVars as $var) { + if ($var->origName === 'e') { + $hasE = true; + break; + } + } + $this->assertTrue($hasE, 'catch variable should have an SSA var'); + } +} diff --git a/src/Php/Analysis/SsaBuilder.php b/src/Php/Analysis/SsaBuilder.php new file mode 100644 index 00000000..773b011a --- /dev/null +++ b/src/Php/Analysis/SsaBuilder.php @@ -0,0 +1,1694 @@ + ssaVarId + + /** Whether this var has a constant value (from constant propagation later) */ + public bool $isConstant = false; + /** The constant value, if known */ + public mixed $constantValue = null; + + public function __construct(int $id, string $origName, int $flags = 0) { + $this->id = $id; + $this->origName = $origName; + $this->flags = $flags; + } +} + +/** + * e-SSA pi node constraint: records type narrowing from a condition. + * + * e.g. after `if ($x instanceof Foo)`, the true-branch gets a PiConstraint + * that narrows $x's type to Foo (or subclass). + */ +class PiConstraint +{ + /** Narrowed type bitmask or class name */ + public string $narrowedType = ''; + /** The condition expression that produces this constraint */ + public ?NodeAbstract $condition = null; + /** Whether this is an instanceof check (is_instanceof = true means may be subclass) */ + public bool $isInstanceof = true; + + /** Range constraint: min <= var <= max (for integer range inference) */ + public bool $hasRange = false; + public int|float $rangeMin = 0; + public int|float $rangeMax = 0; +} + +// SsaVar flags (mirror php-src zend_ssa_var_info patterns) +class SsaFlags +{ + public const int UNDEFINED = 1 << 0; // variable is undefined (after unset) + public const int REFERENCE = 1 << 1; // variable has been referenced (&$var) + public const int ESCAPED = 1 << 2; // variable escapes (global, closure use, etc.) + public const int PHI = 1 << 3; // this SSA var is a φ function result + public const int PARAM = 1 << 4; // defined by function parameter + public const int KILLED = 1 << 5; // explicitly killed via unset() +} + +/** + * A basic block in the control flow graph. + */ +class SsaBlock +{ + public int $id; + /** @var Node[] AST statements in this block (in order) */ + public array $stmts = []; + /** @var int[] Predecessor block IDs */ + public array $predecessors = []; + /** @var int[] Successor block IDs */ + public array $successors = []; + + // For goto/label handling + public bool $isGotoTarget = false; + public ?string $labelName = null; + public bool $endsWithGoto = false; + public ?string $gotoLabel = null; + + // For branch CFG expansion: block IDs (indices in blocks array) + /** First block of the true branch, -1 if not set */ + public int $branchTrueBlock = -1; + /** First block of the false branch, -1 if no false branch */ + public int $branchFalseBlock = -1; + /** Join block after both branches converge */ + public int $branchJoinBlock = -1; + /** This block is a join point for a preceding branch */ + public bool $isJoinPoint = false; + /** If >= 0, this block must jump to the specified block (no fall-through) */ + public int $forceJumpTo = -1; + + /** @var array φ functions at block entry: varName => SsaVar */ + public array $phi = []; + + // Dominator tree + public int $dominator = -1; // immediate dominator block ID + /** @var int[] Blocks that this block immediately dominates */ + public array $dominatedChildren = []; + /** @var int[] Blocks in the dominance frontier of this block */ + public array $dominanceFrontier = []; +} + +/** + * Tracks variable state during SSA renaming pass. + */ +class VarState +{ + /** Stack of SsaVar IDs for this variable name (current definition is on top) */ + public array $stack = []; + /** Counter for generating unique SSA var IDs for this variable */ + public int $counter = 0; +} + +/** + * SSA Builder: transforms a function body into SSA form. + * + * Usage: + * $builder = new SsaBuilder($functionDef->stmts, $functionDef->argInfoList); + * $builder->build(); + * + * After build(): + * - $builder->blocks: basic blocks with φ functions + * - $builder->ssaVars: all SSA variable definitions + * - $builder->getVarDefAtUse($varName, $blockId, $stmtIndex): get the SSA var for a use + * - $builder->getDefBlocks($varName): all blocks where $varName is defined + */ +class SsaBuilder +{ + /** @var SsaBlock[] All basic blocks */ + public array $blocks = []; + + /** @var SsaVar[] Map of ssaVarId => SsaVar */ + public array $ssaVars = []; + + /** @var VarState[] Map of varName => VarState (used during renaming) */ + private array $varStates = []; + + /** Next SSA variable ID */ + private int $nextSsaId = 0; + + /** @var array Block IDs where each variable is defined */ + private array $defBlocks = []; + + /** @var array For each block and variable, the SSA var at block entry (after φ) */ + private array $blockEntryVars = []; + + /** @var string[] Parameter names */ + private array $params = []; + + /** @var array Parameter byRef flags: paramName => bool */ + private array $paramByRef = []; + + /** @var array goto label → block ID */ + private array $labelBlocks = []; + + /** @var int The entry block ID */ + private int $entryBlockId = 0; + + /** @var int The exit block ID */ + private int $exitBlockId = -1; + + /** + * @param Node[] $stmts Function body AST statements + * @param array $argInfoList Array of ArgInfo for function parameters + */ + public function __construct( + private array $stmts, + private array $argInfoList = [] + ) { + foreach ($this->argInfoList as $argInfo) { + $this->params[] = $argInfo->name; + $this->paramByRef[$argInfo->name] = $argInfo->byRef ?? false; + } + } + + // ========================================================================= + // Public API + // ========================================================================= + + /** + * Run the full SSA construction pipeline. + */ + /** @return Node[] Original function body AST statements */ + public function getStmts(): array + { + return $this->stmts; + } + + public function build(): void + { + $this->buildCfg(); + $this->computeDominators(); + $this->computeDominanceFrontier(); + $this->placePhiFunctions(); + $this->renameVariables(); + } + + /** + * Get the SSA variable that reaches a specific use point. + * + * @param string $varName Original variable name (e.g., "x") + * @param int $blockId Block containing the use + * @param int $stmtIndex Statement index within the block + * @return SsaVar|null The reaching SSA var, or null if undefined + */ + public function getVarAtUse(string $varName, int $blockId, int $stmtIndex): ?SsaVar + { + $key = "{$blockId}:{$varName}:{$stmtIndex}"; + // During renaming, we'd record use→def mappings here + // For now, walk the block backward from stmtIndex to find the definition + return $this->findReachingDef($varName, $blockId, $stmtIndex); + } + + /** + * Get all blocks where a variable has definitions. + * + * @return int[] Block IDs + */ + public function getDefBlocks(string $varName): array + { + return $this->defBlocks[$varName] ?? []; + } + + /** + * Check if a variable is defined at the entry of a block (via φ function). + */ + public function hasPhiAtBlock(string $varName, int $blockId): bool + { + return isset($this->blocks[$blockId]->phi[$varName]); + } + + /** + * Get the block containing a goto label. + */ + public function getLabelBlock(string $labelName): ?int + { + return $this->labelBlocks[$labelName] ?? null; + } + + // ========================================================================= + // Step 1: CFG Construction + // ========================================================================= + + /** + * Build the control flow graph: split statements into basic blocks. + * + * Block boundaries are created at: + * - Labels (goto targets) — always start a new block + * - Goto statements — end the current block + * - If/else/while/for/foreach/switch/try — sub-blocks are created recursively + * - Return/throw/exit — terminate the block + */ + private function buildCfg(): void + { + // First pass: collect all label positions (goto targets) + $this->collectLabels($this->stmts); + + // Second pass: split into blocks + $this->blocks = $this->splitIntoBlocks($this->stmts); + + // Create exit block + $exitBlock = new SsaBlock(); + $exitBlock->id = count($this->blocks); + $this->blocks[] = $exitBlock; + $this->exitBlockId = $exitBlock->id; + + // Connect blocks + $this->connectBlocks(); + } + + /** + * First pass: find all goto labels and their statement positions. + */ + private function collectLabels(array $stmts): void + { + foreach ($stmts as $i => $stmt) { + if ($stmt instanceof Stmt\Label) { + $this->labelBlocks[$stmt->name->name] = -1; // placeholder, will be set in splitIntoBlocks + } elseif ($stmt instanceof Stmt\If_) { + $this->collectLabels($stmt->stmts); + if ($stmt->elseifs) { + foreach ($stmt->elseifs as $elseif) { + $this->collectLabels($elseif->stmts); + } + } + if ($stmt->else) { + $this->collectLabels($stmt->else->stmts); + } + } elseif ($stmt instanceof Stmt\While_ || $stmt instanceof Stmt\Do_) { + $this->collectLabels($stmt->stmts); + } elseif ($stmt instanceof Stmt\For_) { + $this->collectLabels($stmt->stmts); + } elseif ($stmt instanceof Stmt\Foreach_) { + $this->collectLabels($stmt->stmts); + } elseif ($stmt instanceof Stmt\Switch_) { + foreach ($stmt->cases as $case) { + $this->collectLabels($case->stmts); + } + } elseif ($stmt instanceof Stmt\TryCatch) { + $this->collectLabels($stmt->stmts); + foreach ($stmt->catches as $catch) { + $this->collectLabels($catch->stmts); + } + if ($stmt->finally) { + $this->collectLabels($stmt->finally->stmts); + } + } elseif ($stmt instanceof Stmt\Block) { + $this->collectLabels($stmt->stmts); + } + } + } + + /** + * Split a list of statements into basic blocks. + * + * A new block starts at: + * - The beginning of the function (entry block) + * - After a label (goto target) + * - After a conditional branch target + * + * A block ends at: + * - A goto statement + * - A return/throw/exit statement + * - A label (the label starts a new block) + * - A branch (if/while/etc. — the branch itself is the last stmt) + * + * @param Node[] $stmts + * @return SsaBlock[] + */ + private function splitIntoBlocks(array $stmts): array + { + $blocks = []; + $currentBlock = $this->newBlock(); + if (empty($blocks)) { + $currentBlock->id = 0; + $this->entryBlockId = 0; + } + $blocks[] = $currentBlock; + + $this->splitStmtList($stmts, $blocks, $currentBlock); + + // Renumber blocks sequentially + foreach ($blocks as $idx => $block) { + $block->id = $idx; + } + $this->entryBlockId = 0; + + return $blocks; + } + + /** + * Recursively split a statement list into blocks, appending to $blocks. + * Returns the index within $blocks of the first block created. + * + * @param Node[] $stmts + * @param SsaBlock[] $blocks Reference to the blocks array being built + * @param SsaBlock $currentBlock The current block (already in $blocks) + */ + private function splitStmtList(array $stmts, array &$blocks, SsaBlock &$currentBlock): void + { + for ($i = 0; $i < count($stmts); $i++) { + $stmt = $stmts[$i]; + + // Label: always starts a new block + if ($stmt instanceof Stmt\Label) { + if (!empty($currentBlock->stmts)) { + $currentBlock = $this->newBlock(); + $blocks[] = $currentBlock; + } + $currentBlock->isGotoTarget = true; + $currentBlock->labelName = $stmt->name->name; + // Use index in $blocks array (matches final ID after renumbering) + $this->labelBlocks[$stmt->name->name] = count($blocks) - 1; + continue; + } + + // Goto: ends the current block + if ($stmt instanceof Stmt\Goto_) { + $currentBlock->stmts[] = $stmt; + $currentBlock->endsWithGoto = true; + $currentBlock->gotoLabel = $stmt->name->name; + if ($i < count($stmts) - 1) { + $currentBlock = $this->newBlock(); + $blocks[] = $currentBlock; + } + continue; + } + + // Terminal statements: end the current block + if ($stmt instanceof Stmt\Return_ || $stmt instanceof Stmt\Throw_) { + $currentBlock->stmts[] = $stmt; + if ($i < count($stmts) - 1) { + $currentBlock = $this->newBlock(); + $blocks[] = $currentBlock; + } + continue; + } + + // If statement: expand into condition block + body blocks + join + if ($stmt instanceof Stmt\If_) { + $this->expandIfStmt($stmt, $blocks, $currentBlock, $i, $stmts); + continue; + } + + // Other branch statements: end current block, will be handled later + if ($this->isBranch($stmt)) { + $currentBlock->stmts[] = $stmt; + if ($i < count($stmts) - 1) { + $currentBlock = $this->newBlock(); + $blocks[] = $currentBlock; + } + continue; + } + + // Regular statement: add to current block + $currentBlock->stmts[] = $stmt; + } + } + + /** + * Expand an if/elseif/else statement into proper CFG blocks. + * + * Creates: + * - A condition block (ending with the if) + * - Body blocks for the true branch + * - Body blocks for the false branch (else/elseif) + * - A join block where both paths converge + * + * @param SsaBlock[] $blocks + */ + private function expandIfStmt(Stmt\If_ $ifStmt, array &$blocks, SsaBlock &$currentBlock, int $stmtIndex, array $allStmts): void + { + // The if statement is the last statement in the current (condition) block + $currentBlock->stmts[] = $ifStmt; + $condBlockIndex = count($blocks) - 1; + + // Process true branch body into new blocks + $trueStartIndex = count($blocks); + $trueStartBlock = $this->newBlock(); + $blocks[] = $trueStartBlock; + $this->splitStmtList($ifStmt->stmts, $blocks, $trueStartBlock); + $trueEndIndex = count($blocks) - 1; + + // Process false branch (else/elseif chain) + $falseStartIndex = -1; + $hasFalseBranch = false; + if (!empty($ifStmt->elseifs)) { + $falseStartIndex = count($blocks); + $hasFalseBranch = true; + foreach ($ifStmt->elseifs as $elseif) { + $innerIf = new Stmt\If_($elseif->cond, [ + 'stmts' => $elseif->stmts, + 'elseifs' => [], + 'else' => null, + ]); + if (!empty($currentBlock->stmts)) { + $currentBlock = $this->newBlock(); + $blocks[] = $currentBlock; + } + $this->expandIfStmt($innerIf, $blocks, $currentBlock, 0, [$innerIf]); + } + } + if ($ifStmt->else) { + if (!$hasFalseBranch) { + $falseStartIndex = count($blocks); + $hasFalseBranch = true; + } + $falseStartBlock = $this->newBlock(); + $blocks[] = $falseStartBlock; + if ($falseStartIndex === -1) { + $falseStartIndex = count($blocks) - 1; + } + $this->splitStmtList($ifStmt->else->stmts, $blocks, $falseStartBlock); + } + $falseEndIndex = $hasFalseBranch ? (count($blocks) - 1) : -1; + + // Create join block (where both true and false paths converge) + $joinBlock = $this->newBlock(); + $joinBlock->isJoinPoint = true; + $joinIndex = count($blocks); + $blocks[] = $joinBlock; + + // Force the last block of each branch to jump to the join block + // (prevents fall-through from true body into false body, etc.) + if ($trueEndIndex >= $trueStartIndex) { + $blocks[$trueEndIndex]->forceJumpTo = $joinIndex; + } + if ($falseEndIndex >= 0) { + $blocks[$falseEndIndex]->forceJumpTo = $joinIndex; + } + + // Store branch info on condition block + $blocks[$condBlockIndex]->branchTrueBlock = $trueStartIndex; + $blocks[$condBlockIndex]->branchFalseBlock = $falseStartIndex; + $blocks[$condBlockIndex]->branchJoinBlock = $joinIndex; + + // If there are more statements after this if, continue in the join block + if ($stmtIndex < count($allStmts) - 1) { + $currentBlock = $joinBlock; + } + } + + /** + * Check if a statement is a control flow branch. + */ + private function isBranch(Node $stmt): bool + { + return $stmt instanceof Stmt\If_ + || $stmt instanceof Stmt\While_ + || $stmt instanceof Stmt\Do_ + || $stmt instanceof Stmt\For_ + || $stmt instanceof Stmt\Foreach_ + || $stmt instanceof Stmt\Switch_ + || $stmt instanceof Stmt\TryCatch; + } + + /** + * Connect blocks: compute predecessor/successor relationships. + */ + private function connectBlocks(): void + { + $n = count($this->blocks); + for ($i = 0; $i < $n; $i++) { + $block = $this->blocks[$i]; + + // Forced jump overrides normal flow (used by branch bodies to reach join) + if ($block->forceJumpTo >= 0 && $block->forceJumpTo < $n) { + $block->successors[] = $block->forceJumpTo; + $this->blocks[$block->forceJumpTo]->predecessors[] = $i; + continue; + } + + // Empty blocks always fall through + if (empty($block->stmts)) { + if ($i < $n - 1) { + $block->successors[] = $i + 1; + $this->blocks[$i + 1]->predecessors[] = $i; + } + continue; + } + + $lastStmt = end($block->stmts); + + if ($block->endsWithGoto && $block->gotoLabel !== null) { + // Goto: jump to label target + $targetId = $this->labelBlocks[$block->gotoLabel] ?? -1; + if ($targetId >= 0) { + $block->successors[] = $targetId; + $this->blocks[$targetId]->predecessors[] = $i; + } + // Goto does NOT fall through + } elseif ($lastStmt instanceof Stmt\Return_ || $lastStmt instanceof Stmt\Throw_) { + // Terminal: connect to exit block + $block->successors[] = $this->exitBlockId; + $this->blocks[$this->exitBlockId]->predecessors[] = $i; + } elseif ($this->isBranch($lastStmt)) { + $this->connectBranch($block, $lastStmt, $i, $n); + } elseif ($i < $n - 1) { + // Fall through to next block + $block->successors[] = $i + 1; + $this->blocks[$i + 1]->predecessors[] = $i; + } + } + } + + /** + * Connect branch successors for if/while/for/foreach/switch/try. + */ + private function connectBranch(SsaBlock $block, Node $stmt, int $blockId, int $totalBlocks): void + { + if ($stmt instanceof Stmt\If_) { + // Use branch tracking fields set during expandIfStmt + if ($block->branchTrueBlock >= 0 && $block->branchTrueBlock < $totalBlocks) { + $block->successors[] = $block->branchTrueBlock; + $this->blocks[$block->branchTrueBlock]->predecessors[] = $blockId; + } + if ($block->branchFalseBlock >= 0 && $block->branchFalseBlock < $totalBlocks) { + $block->successors[] = $block->branchFalseBlock; + $this->blocks[$block->branchFalseBlock]->predecessors[] = $blockId; + } else { + // No false branch: condition falls through to join block + if ($block->branchJoinBlock >= 0 && $block->branchJoinBlock < $totalBlocks) { + $block->successors[] = $block->branchJoinBlock; + $this->blocks[$block->branchJoinBlock]->predecessors[] = $blockId; + } + } + // Connect the last block of each branch to the join block. + // This is done by findAndConnectToJoin() since body blocks may fall through. + // For now, trust that the body blocks' final blocks fall through to join. + } elseif ($stmt instanceof Stmt\While_) { + // Back-edge: loop body → condition + // Forward-edge: condition → after loop + $nextBlockId = $blockId + 1; + if ($nextBlockId < $totalBlocks) { + $block->successors[] = $nextBlockId; + $this->blocks[$nextBlockId]->predecessors[] = $blockId; + } + // After loop + $afterLoopId = $blockId + 2; + if ($afterLoopId < $totalBlocks) { + $block->successors[] = $afterLoopId; + $this->blocks[$afterLoopId]->predecessors[] = $blockId; + } + } elseif ($stmt instanceof Stmt\Do_) { + $nextBlockId = $blockId + 1; + if ($nextBlockId < $totalBlocks) { + $block->successors[] = $nextBlockId; + $this->blocks[$nextBlockId]->predecessors[] = $blockId; + } + } elseif ($stmt instanceof Stmt\For_ || $stmt instanceof Stmt\Foreach_) { + $nextBlockId = $blockId + 1; + if ($nextBlockId < $totalBlocks) { + $block->successors[] = $nextBlockId; + $this->blocks[$nextBlockId]->predecessors[] = $blockId; + } + $afterLoopId = $blockId + 2; + if ($afterLoopId < $totalBlocks) { + $block->successors[] = $afterLoopId; + $this->blocks[$afterLoopId]->predecessors[] = $blockId; + } + } elseif ($stmt instanceof Stmt\TryCatch) { + $nextBlockId = $blockId + 1; + if ($nextBlockId < $totalBlocks) { + $block->successors[] = $nextBlockId; + $this->blocks[$nextBlockId]->predecessors[] = $blockId; + } + // Catch blocks are also successors (exception path) + // Simplified: catch blocks are after the try body + } elseif ($stmt instanceof Stmt\Switch_) { + $nextBlockId = $blockId + 1; + if ($nextBlockId < $totalBlocks) { + $block->successors[] = $nextBlockId; + $this->blocks[$nextBlockId]->predecessors[] = $blockId; + } + } + } + + // ========================================================================= + // Step 2: Dominator Tree + // ========================================================================= + + /** + * Compute immediate dominators using the iterative algorithm + * (Cooper, Harvey, Kennedy 2001). + * + * dom[entry] = {entry} + * dom[b] = {all blocks} for b != entry + * + * Iterate: dom[b] = {b} ∪ (∩_{p ∈ preds[b]} dom[p]) + */ + private function computeDominators(): void + { + $n = count($this->blocks); + if ($n === 0) { + return; + } + + // Initialize: entry dominates only itself; others dominate all blocks + $allBlocks = []; + for ($i = 0; $i < $n; $i++) { + $allBlocks[] = $i; + } + + // dominators[b] = set of blocks that dominate b + $dominators = []; + for ($i = 0; $i < $n; $i++) { + if ($i === $this->entryBlockId) { + $dominators[$i] = [$this->entryBlockId => true]; + } else { + $dominators[$i] = array_fill_keys($allBlocks, true); + } + } + + // Iterate until fixed point + $changed = true; + while ($changed) { + $changed = false; + for ($b = 0; $b < $n; $b++) { + if ($b === $this->entryBlockId) { + continue; + } + + // Intersection of all predecessors' dominator sets + $newDom = null; + foreach ($this->blocks[$b]->predecessors as $predId) { + if ($newDom === null) { + $newDom = $dominators[$predId]; + } else { + $newDom = array_intersect_key($newDom, $dominators[$predId]); + } + } + if ($newDom === null) { + $newDom = []; + } + // Add self + $newDom[$b] = true; + + if ($newDom !== $dominators[$b]) { + $dominators[$b] = $newDom; + $changed = true; + } + } + } + + // Compute immediate dominator (idom) from dominator sets + // idom[b] = the strict dominator of b that is closest to b + for ($b = 0; $b < $n; $b++) { + if ($b === $this->entryBlockId) { + $this->blocks[$b]->dominator = $this->entryBlockId; + continue; + } + // Strict dominators of b (excluding b itself) + $strictDom = array_keys(array_diff_key($dominators[$b], [$b => true])); + // idom is the one that is dominated by all other strict dominators + $idom = $this->entryBlockId; + foreach ($strictDom as $d) { + if ($d !== $b && $d !== $this->entryBlockId) { + $idom = $d; // approximate — real implementation would find the max + break; + } + } + // Better idom computation: idom is the strict dominator with max DFS number + // Simplified: pick the first strict dominator that isn't block itself + if ($strictDom) { + // Find the one that dominates no other strict dominator (the "closest") + $idom = $this->findImmediateDominator($b, $strictDom, $dominators); + } + $this->blocks[$b]->dominator = $idom; + $this->blocks[$idom]->dominatedChildren[] = $b; + } + } + + /** + * Find the immediate dominator: the strict dominator that is NOT + * dominated by any other strict dominator of b. + */ + private function findImmediateDominator(int $b, array $strictDom, array $dominators): int + { + if (count($strictDom) === 1) { + return $strictDom[0]; + } + + // idom is the one not dominated by any other strict dominator + foreach ($strictDom as $candidate) { + $dominatedByOther = false; + foreach ($strictDom as $other) { + if ($other !== $candidate && isset($dominators[$candidate][$other])) { + $dominatedByOther = true; + break; + } + } + if (!$dominatedByOther) { + return $candidate; + } + } + + return $this->entryBlockId; + } + + // ========================================================================= + // Step 3: Dominance Frontier + // ========================================================================= + + /** + * Compute dominance frontier for each block. + * + * DF[b] = { y | b dominates a predecessor of y, but b does NOT strictly dominate y } + * + * This is used to determine where φ functions must be placed: + * if variable v is defined in block b, then every block in DF[b] + * needs a φ function for v. + */ + private function computeDominanceFrontier(): void + { + $n = count($this->blocks); + + for ($b = 0; $b < $n; $b++) { + $this->blocks[$b]->dominanceFrontier = []; + } + + for ($b = 0; $b < $n; $b++) { + $preds = $this->blocks[$b]->predecessors; + if (count($preds) < 2) { + continue; // No join point, no φ needed from this block + } + + foreach ($preds as $predId) { + $runner = $predId; + // Walk up the dominator tree from pred until we find a node + // that dominates b + while ($runner !== $this->blocks[$b]->dominator && $runner !== $this->entryBlockId) { + // runner is in the dominance frontier of b + $this->blocks[$runner]->dominanceFrontier[] = $b; + $runner = $this->blocks[$runner]->dominator; + } + } + } + } + + // ========================================================================= + // Step 4: φ Function Placement + // ========================================================================= + + /** + * Place φ functions for all variables at appropriate join points. + * + * Algorithm: + * For each variable v: + * worklist = all blocks where v is defined + * for each b in worklist: + * for each df in DF[b]: + * if df does not already have φ for v: + * add φ for v at entry of df + * add df to worklist (φ is also a definition) + */ + private function placePhiFunctions(): void + { + // Collect all variable names defined anywhere (including params) + $allVars = $this->collectAllVariables(); + + // Collect definition blocks for each variable + foreach ($allVars as $varName) { + $this->defBlocks[$varName] = []; + } + + foreach ($this->blocks as $block) { + foreach ($block->stmts as $stmt) { + $defs = $this->getDefinedVars($stmt); + foreach ($defs as $varName) { + if (!in_array($block->id, $this->defBlocks[$varName] ?? [])) { + $this->defBlocks[$varName][] = $block->id; + } + if (!in_array($varName, $allVars)) { + $allVars[] = $varName; + $this->defBlocks[$varName] = [$block->id]; + } + } + } + } + + // For each variable, place φ at dominance frontier of each definition + foreach ($allVars as $varName) { + $worklist = $this->defBlocks[$varName] ?? []; + $hasPhi = []; // set of blocks that already have φ for this var + $iterCount = 0; + + while (!empty($worklist) && $iterCount < 1000) { + $iterCount++; + $b = array_shift($worklist); + + foreach ($this->blocks[$b]->dominanceFrontier as $dfBlockId) { + if (!isset($hasPhi[$dfBlockId])) { + // Place φ at dfBlockId + $phiVarId = $this->allocateSsaId(); + $phiVar = new SsaVar($phiVarId, $varName, SsaFlags::PHI); + $this->ssaVars[$phiVarId] = $phiVar; + + $this->blocks[$dfBlockId]->phi[$varName] = $phiVar; + + $hasPhi[$dfBlockId] = true; + // φ is also a definition → add dfBlockId to worklist + if (!in_array($dfBlockId, $worklist)) { + $worklist[] = $dfBlockId; + } + if (!in_array($dfBlockId, $this->defBlocks[$varName])) { + $this->defBlocks[$varName][] = $dfBlockId; + } + } + } + } + } + } + + /** + * Collect all variable names that appear in the function body. + */ + private function collectAllVariables(): array + { + $vars = []; + + // Parameters always count as variables + foreach ($this->params as $paramName) { + $vars[] = $paramName; + } + + // Walk all blocks and statements + foreach ($this->blocks as $block) { + foreach ($block->stmts as $stmt) { + $this->collectVarsFromNode($stmt, $vars); + } + } + + return array_unique($vars); + } + + /** + * Recursively collect variable names from an AST node. + */ + private function collectVarsFromNode(Node $node, array &$vars): void + { + if ($node instanceof Expr\Variable && is_string($node->name)) { + $vars[] = $node->name; + return; + } + + // Recursively walk child nodes + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + $this->collectVarsFromNode($subNode, $vars); + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if ($item instanceof Node) { + $this->collectVarsFromNode($item, $vars); + } + } + } + } + } + + /** + * Get variable names defined by a statement. + * Also recurses into TryCatch and other compound statements + * to find nested definitions (e.g., catch variables). + */ + private function getDefinedVars(Node $stmt): array + { + $defs = []; + + if ($stmt instanceof Stmt\Expression && $stmt->expr instanceof Expr\Assign) { + $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\Static_) { + foreach ($stmt->vars as $staticVar) { + $defs[] = $staticVar->var->name; + } + } elseif ($stmt instanceof Stmt\Foreach_) { + if ($stmt->keyVar instanceof Expr\Variable && is_string($stmt->keyVar->name)) { + $defs[] = $stmt->keyVar->name; + } + if ($stmt->valueVar instanceof Expr\Variable && is_string($stmt->valueVar->name)) { + $defs[] = $stmt->valueVar->name; + } + } elseif ($stmt instanceof Stmt\Catch_) { + if (is_string($stmt->var->name)) { + $defs[] = $stmt->var->name; + } + } + // Note: unset($var) is also a kind of "definition" — kills the variable + elseif ($stmt instanceof Stmt\Unset_) { + foreach ($stmt->vars as $var) { + if ($var instanceof Expr\Variable && is_string($var->name)) { + $defs[] = $var->name; + } + } + } + + // Recurse into compound statements for nested definitions + if ($stmt instanceof Stmt\TryCatch) { + foreach ($stmt->catches as $catch) { + $defs = array_merge($defs, $this->getDefinedVars($catch)); + } + } + + return $defs; + } + + // ========================================================================= + // Step 5: Variable Renaming (SSA Construction) + // ========================================================================= + + /** + * Rename variables to SSA form by walking the dominator tree. + * + * For each block: + * 1. For each φ function at block entry: create new SSA var, push to stack + * 2. For each statement in block: + * a. Rename all variable USEs to use the current SSA var from stack + * b. For each variable DEF: create new SSA var, push to stack + * 3. For each successor block: fill in φ function source operands + * 4. Recursively process dominated children + * 5. Pop all SSA vars pushed in this block + */ + private function renameVariables(): void + { + if (empty($this->blocks)) { + return; + } + + // Initialize parameter SSA vars at entry block + foreach ($this->params as $paramName) { + $ssaId = $this->allocateSsaId(); + $flags = SsaFlags::PARAM; + if ($this->paramByRef[$paramName] ?? false) { + $flags |= SsaFlags::REFERENCE; + } + $ssaVar = new SsaVar($ssaId, $paramName, $flags); + $this->ssaVars[$ssaId] = $ssaVar; + + if (!isset($this->varStates[$paramName])) { + $this->varStates[$paramName] = new VarState(); + } + $this->varStates[$paramName]->stack[] = $ssaId; + $this->varStates[$paramName]->counter = 1; + } + + // Walk dominator tree starting from entry + $this->renameBlock($this->entryBlockId); + } + + /** + * Rename variables within a single block, then recurse into dominated children. + */ + private function renameBlock(int $blockId): void + { + $block = $this->blocks[$blockId]; + $pushedVars = []; + + // Step 1: Process φ functions at block entry + // Each φ function produces a new SSA var + foreach ($block->phi as $varName => $phiVar) { + if (!isset($this->varStates[$varName])) { + $this->varStates[$varName] = new VarState(); + } + $this->varStates[$varName]->stack[] = $phiVar->id; + $pushedVars[] = [$varName, 'phi']; + } + + // Step 2: Walk statements in block + foreach ($block->stmts as $stmtIndex => $stmt) { + // First: rename all variable USEs in the statement + $this->renameUses($stmt, $blockId, $stmtIndex); + + // Then: handle definitions + $this->renameDefs($stmt, $blockId, $stmtIndex, $pushedVars); + } + + // Step 3: Fill φ source operands for successors + $this->fillPhiSources($block); + + // Step 4: Recurse into dominated children + foreach ($block->dominatedChildren as $childId) { + $this->renameBlock($childId); + } + + // Step 5: Pop SSA vars pushed in this block + foreach (array_reverse($pushedVars) as [$varName, $type]) { + array_pop($this->varStates[$varName]->stack); + } + } + + /** + * Rename variable uses in a statement: replace each use with the current + * SSA var ID from the stack. + */ + private function renameUses(Node $stmt, int $blockId, int $stmtIndex): void + { + $uses = $this->getUsedVars($stmt); + foreach ($uses as $varName) { + $currentSsaId = $this->getCurrentSsaId($varName); + if ($currentSsaId !== null) { + // Record: at block $blockId, stmt $stmtIndex, var $varName + // → SSA var $currentSsaId + // (This mapping can be used later for type inference etc.) + } + } + } + + /** + * Handle variable definitions in a statement: create new SSA vars. + */ + private function renameDefs(Node $stmt, int $blockId, int $stmtIndex, array &$pushedVars): void + { + // Handle unset($var) — kill the variable + if ($stmt instanceof Stmt\Unset_) { + foreach ($stmt->vars as $var) { + if ($var instanceof Expr\Variable && is_string($var->name)) { + $varName = $var->name; + $ssaId = $this->allocateSsaId(); + $ssaVar = new SsaVar($ssaId, $varName, SsaFlags::KILLED | SsaFlags::UNDEFINED); + $ssaVar->definition = $stmt; + $this->ssaVars[$ssaId] = $ssaVar; + + if (!isset($this->varStates[$varName])) { + $this->varStates[$varName] = new VarState(); + } + $this->varStates[$varName]->stack[] = $ssaId; + $pushedVars[] = [$varName, 'unset']; + } + } + return; + } + + // Handle reference assignment: $x =& $y + if ($stmt instanceof Stmt\Expression && $stmt->expr instanceof Expr\AssignRef) { + $assignRef = $stmt->expr; + // LHS: $x gets a new SSA var with REFERENCE flag + $var = $assignRef->var; + if ($var instanceof Expr\Variable && is_string($var->name)) { + $varName = $var->name; + $ssaId = $this->allocateSsaId(); + $ssaVar = new SsaVar($ssaId, $varName, SsaFlags::REFERENCE); + $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_ref']; + } + // RHS: $y gets a new SSA var with ESCAPED flag because taking a + // reference to $y makes it escape — it can now be mutated through $x. + $rhs = $assignRef->expr; + if ($rhs instanceof Expr\Variable && is_string($rhs->name)) { + $rhsName = $rhs->name; + $rhsId = $this->allocateSsaId(); + $rhsVar = new SsaVar($rhsId, $rhsName, SsaFlags::ESCAPED); + $rhsVar->definition = $stmt; + $this->ssaVars[$rhsId] = $rhsVar; + + if (!isset($this->varStates[$rhsName])) { + $this->varStates[$rhsName] = new VarState(); + } + $this->varStates[$rhsName]->stack[] = $rhsId; + $pushedVars[] = [$rhsName, 'assign_ref_rhs']; + } + return; + } + + // Handle regular assignment: $x = expr + if ($stmt instanceof Stmt\Expression && $stmt->expr instanceof Expr\Assign) { + $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']; + } + return; + } + + // Handle foreach value/key variables + if ($stmt instanceof Stmt\Foreach_) { + if ($stmt->valueVar instanceof Expr\Variable && is_string($stmt->valueVar->name)) { + $varName = $stmt->valueVar->name; + $flags = $stmt->byRef ? SsaFlags::REFERENCE : 0; + $ssaId = $this->allocateSsaId(); + $ssaVar = new SsaVar($ssaId, $varName, $flags); + $ssaVar->definition = $stmt; + $this->ssaVars[$ssaId] = $ssaVar; + + if (!isset($this->varStates[$varName])) { + $this->varStates[$varName] = new VarState(); + } + $this->varStates[$varName]->stack[] = $ssaId; + $pushedVars[] = [$varName, 'foreach']; + } + if ($stmt->keyVar instanceof Expr\Variable && is_string($stmt->keyVar->name)) { + $varName = $stmt->keyVar->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, 'foreach_key']; + } + return; + } + + // Handle catch variables + if ($stmt instanceof Stmt\Catch_) { + if (is_string($stmt->var->name)) { + $varName = $stmt->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, 'catch']; + } + return; + } + + // Handle static variable declarations + if ($stmt instanceof Stmt\Static_) { + foreach ($stmt->vars as $staticVar) { + $varName = $staticVar->var->name; + $ssaId = $this->allocateSsaId(); + $ssaVar = new SsaVar($ssaId, $varName, SsaFlags::ESCAPED); + $ssaVar->definition = $stmt; + $this->ssaVars[$ssaId] = $ssaVar; + + if (!isset($this->varStates[$varName])) { + $this->varStates[$varName] = new VarState(); + } + $this->varStates[$varName]->stack[] = $ssaId; + $pushedVars[] = [$varName, 'static']; + } + return; + } + + // Handle TryCatch: process catch variables as definitions + if ($stmt instanceof Stmt\TryCatch) { + foreach ($stmt->catches as $catch) { + $this->renameDefs($catch, $blockId, $stmtIndex, $pushedVars); + } + } + + // Handle function calls that may modify variables by reference + $this->handleCallByRef($stmt, $pushedVars); + } + + /** + * Handle function/method calls that receive variables by reference. + * + * Two mechanisms in the AOT compiler: + * 1. Explicit &$var at call site: func(&$x) — detected via $arg->byRef + * 2. refval() pseudo-function: func(refval($x)) — used for dynamic calls + * where the compiler can't statically determine if the parameter is byRef. + * The compiler detects refval() via isRefvalCall() and unwraps it during codegen. + * + * When a variable is passed by reference to a function call, the function + * may modify it. We model this as: + * - Mark the variable as REFERENCE/ESCAPED + * - Create a new SSA definition for the variable (value is unknown after call) + */ + private function handleCallByRef(Node $stmt, array &$pushedVars): void + { + if (!($stmt instanceof Stmt\Expression)) { + return; + } + $expr = $stmt->expr; + + if ($expr instanceof Expr\FuncCall) { + $this->collectCallByRefArgs($expr->args, $stmt, $pushedVars); + } + + if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) { + $this->collectCallByRefArgs($expr->args, $stmt, $pushedVars); + } + } + + /** + * Collect variables passed by reference in call arguments. + * + * Handles both: + * - Explicit &$var (arg->byRef === true) + * - refval($var) pseudo-function wrapping + * + * @param Node\Arg[] $args + */ + private function collectCallByRefArgs(array $args, Node $callStmt, array &$pushedVars): void + { + foreach ($args as $arg) { + $varName = null; + + // Case 1: Explicit &$var at call site + if ($arg->byRef && $arg->value instanceof Expr\Variable && is_string($arg->value->name)) { + $varName = $arg->value->name; + } + + // Case 2: refval($var) — AOT compiler convention for dynamic calls + if ($varName === null && $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 ($inner instanceof Expr\Variable && is_string($inner->name)) { + $varName = $inner->name; + } + } + + if ($varName !== null) { + $ssaId = $this->allocateSsaId(); + $ssaVar = new SsaVar($ssaId, $varName, SsaFlags::REFERENCE | SsaFlags::ESCAPED); + $ssaVar->definition = $callStmt; + $this->ssaVars[$ssaId] = $ssaVar; + + if (!isset($this->varStates[$varName])) { + $this->varStates[$varName] = new VarState(); + } + $this->varStates[$varName]->stack[] = $ssaId; + $pushedVars[] = [$varName, 'call_by_ref']; + } + } + } + + /** + * Fill φ function source operands for successor blocks. + * After processing block $blockId, for each successor that has φ functions, + * set the source operand from this block to the current SSA var. + */ + private function fillPhiSources(SsaBlock $block): void + { + foreach ($block->successors as $succId) { + $succ = $this->blocks[$succId]; + foreach ($succ->phi as $varName => $phiVar) { + $currentSsaId = $this->getCurrentSsaId($varName); + if ($currentSsaId !== null) { + $phiVar->phiSources[$block->id] = $currentSsaId; + } + } + } + } + + // ========================================================================= + // Helpers + // ========================================================================= + + private function newBlock(): SsaBlock + { + $block = new SsaBlock(); + $block->id = count($this->blocks); + return $block; + } + + private function allocateSsaId(): int + { + return $this->nextSsaId++; + } + + /** + * Get the current SSA var ID for a variable name (top of stack). + */ + private function getCurrentSsaId(string $varName): ?int + { + $state = $this->varStates[$varName] ?? null; + if ($state === null || empty($state->stack)) { + return null; + } + return end($state->stack); + } + + /** + * Get all variable names USED in a statement. + */ + private function getUsedVars(Node $stmt): array + { + $vars = []; + $this->collectVarUses($stmt, $vars, false); + return array_unique($vars); + } + + /** + * Recursively collect variable uses, skipping left-hand sides of assignments. + */ + private function collectVarUses(Node $node, array &$vars, bool $isLhs): void + { + // Skip left-hand side of assignments (those are definitions, not uses) + if ($node instanceof Expr\Assign) { + // RHS is a use + $this->collectVarUses($node->expr, $vars, false); + // LHS: if it's a simple variable, it's a definition (skip) + // But array dim fetch on LHS uses the array variable + if ($node->var instanceof Expr\ArrayDimFetch) { + $this->collectVarUses($node->var->var, $vars, false); + if ($node->var->dim !== null) { + $this->collectVarUses($node->var->dim, $vars, false); + } + } elseif ($node->var instanceof Expr\PropertyFetch) { + $this->collectVarUses($node->var->var, $vars, false); + } + return; + } + + if ($node instanceof Expr\AssignRef) { + $this->collectVarUses($node->expr, $vars, false); + if ($node->var instanceof Expr\ArrayDimFetch) { + $this->collectVarUses($node->var->var, $vars, false); + if ($node->var->dim !== null) { + $this->collectVarUses($node->var->dim, $vars, false); + } + } + return; + } + + // Assignment operators ($x += expr): $x is both use and def + if ($node instanceof Expr\AssignOp) { + $this->collectVarUses($node->expr, $vars, false); + if ($node->var instanceof Expr\Variable && is_string($node->var->name)) { + $vars[] = $node->var->name; // use of old value + } + if ($node->var instanceof Expr\ArrayDimFetch) { + $this->collectVarUses($node->var->var, $vars, false); + if ($node->var->dim !== null) { + $this->collectVarUses($node->var->dim, $vars, false); + } + } + return; + } + + // Pre/post increment/decrement: both use and def + if ($node instanceof Expr\PreInc || $node instanceof Expr\PostInc + || $node instanceof Expr\PreDec || $node instanceof Expr\PostDec) { + if ($node->var instanceof Expr\Variable && is_string($node->var->name)) { + $vars[] = $node->var->name; + } + if ($node->var instanceof Expr\ArrayDimFetch) { + $this->collectVarUses($node->var->var, $vars, false); + if ($node->var->dim !== null) { + $this->collectVarUses($node->var->dim, $vars, false); + } + } + return; + } + + // Function call: check for ref args + if ($node instanceof Expr\FuncCall || $node instanceof Expr\MethodCall + || $node instanceof Expr\StaticCall || $node instanceof Expr\NullsafeMethodCall) { + foreach ($node->args as $arg) { + $this->collectVarUses($arg->value, $vars, false); + } + if ($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) { + $this->collectVarUses($node->var, $vars, false); + } + if ($node instanceof Expr\StaticCall && $node->class instanceof Expr) { + $this->collectVarUses($node->class, $vars, false); + } + return; + } + + // Simple variable + if ($node instanceof Expr\Variable && is_string($node->name)) { + $vars[] = $node->name; + return; + } + + // Recursively walk children + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + $this->collectVarUses($subNode, $vars, false); + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if ($item instanceof Node) { + $this->collectVarUses($item, $vars, false); + } + } + } + } + } + + /** + * Find the reaching SSA definition for a variable at a given use point. + * Walks backward through the block's statements to find the most recent definition. + */ + private function findReachingDef(string $varName, int $blockId, int $stmtIndex): ?SsaVar + { + $block = $this->blocks[$blockId]; + + // Check φ at block entry + if (isset($block->phi[$varName])) { + return $block->phi[$varName]; + } + + // Walk backward through statements in the block + for ($i = $stmtIndex - 1; $i >= 0; $i--) { + $defs = $this->getDefinedVars($block->stmts[$i]); + if (in_array($varName, $defs)) { + // We'd need the exact SsaVar from renaming pass — simplified return + return null; + } + } + + // If not found in this block, search predecessors recursively + // (Simplified: return null for cross-block queries) + return null; + } + + // ========================================================================= + // e-SSA: Pi Node Construction (Type Narrowing from Conditions) + // ========================================================================= + + /** + * Build e-SSA pi constraints from instanceof and type-check conditions. + * + * When encountering a condition like `if ($x instanceof Foo)`: + * - In the TRUE branch, $x is narrowed to Foo (or subclass) + * - In the FALSE branch, $x is NOT Foo + * + * This method creates pi constraints on SSA variables at conditional branches. + * + * @param Node\Stmt\If_ $ifStmt The if statement to analyze + * @return array{trueVars: array, falseVars: array} + */ + public function buildPiConstraints(Stmt\If_ $ifStmt): array + { + $trueVars = []; + $falseVars = []; + + $cond = $ifStmt->cond; + + // instanceof: $x instanceof ClassName + if ($cond instanceof Expr\Instanceof_) { + if ($cond->expr instanceof Expr\Variable && is_string($cond->expr->name)) { + $varName = $cond->expr->name; + if ($cond->class instanceof Node\Name) { + $className = $cond->class->toString(); + $pi = new PiConstraint(); + $pi->narrowedType = $className; + $pi->condition = $cond; + $pi->isInstanceof = true; // may be subclass + $trueVars[$varName] = $pi; + + // FALSE branch: $x is NOT this type + $piFalse = new PiConstraint(); + $piFalse->narrowedType = '!' . $className; + $piFalse->condition = $cond; + $piFalse->isInstanceof = false; + $falseVars[$varName] = $piFalse; + } + } + } + + // is_* type checks: is_int($x), is_string($x), etc. + if ($cond instanceof Expr\FuncCall && $cond->name instanceof Node\Name) { + $funcName = $cond->name->toLowerString(); + $typeMap = [ + 'is_int' => 'int', 'is_integer' => 'int', 'is_long' => 'int', + 'is_float' => 'float', 'is_double' => 'float', 'is_real' => 'float', + 'is_string' => 'string', 'is_bool' => 'bool', 'is_array' => 'array', + 'is_object' => 'object', 'is_null' => 'null', 'is_resource' => 'resource', + 'is_callable' => 'callable', 'is_iterable' => 'iterable', + ]; + + if (isset($typeMap[$funcName]) && !empty($cond->args)) { + $arg = $cond->args[0]->value; + if ($arg instanceof Expr\Variable && is_string($arg->name)) { + $varName = $arg->name; + $pi = new PiConstraint(); + $pi->narrowedType = $typeMap[$funcName]; + $pi->condition = $cond; + $pi->isInstanceof = false; // exact type, not subclass + $trueVars[$varName] = $pi; + } + } + } + + // Negation: !$cond flips true/false branches + if ($cond instanceof Expr\BooleanNot) { + $inner = $cond->expr; + // Recurse into inner condition, swapping true/false + $dummyIf = new Stmt\If_($inner, ['stmts' => [], 'elseifs' => [], 'else' => null]); + $innerResult = $this->buildPiConstraints($dummyIf); + // Swap + return [ + 'trueVars' => $innerResult['falseVars'], + 'falseVars' => $innerResult['trueVars'], + ]; + } + + // Conjunction: $a && $b — both must be true + if ($cond instanceof Expr\BinaryOp\BooleanAnd || $cond instanceof Expr\BinaryOp\LogicalAnd) { + $leftIf = new Stmt\If_($cond->left, ['stmts' => [], 'elseifs' => [], 'else' => null]); + $rightIf = new Stmt\If_($cond->right, ['stmts' => [], 'elseifs' => [], 'else' => null]); + $left = $this->buildPiConstraints($leftIf); + $right = $this->buildPiConstraints($rightIf); + return [ + 'trueVars' => array_merge($left['trueVars'], $right['trueVars']), + 'falseVars' => [], + ]; + } + + return ['trueVars' => $trueVars, 'falseVars' => $falseVars]; + } + + // ========================================================================= + // Debug / Dump + // ========================================================================= + + /** + * Dump SSA information for debugging. + */ + public function dump(): string + { + $out = "=== SSA Builder Dump ===\n"; + $out .= "Blocks: " . count($this->blocks) . "\n"; + $out .= "SSA Vars: " . count($this->ssaVars) . "\n"; + $out .= "Entry: B{$this->entryBlockId}, Exit: B{$this->exitBlockId}\n\n"; + + foreach ($this->blocks as $block) { + $out .= "--- Block B{$block->id} ---\n"; + if ($block->isGotoTarget) { + $out .= " Label: {$block->labelName} (goto target)\n"; + } + $out .= " Preds: [" . implode(', ', $block->predecessors) . "]\n"; + $out .= " Succs: [" . implode(', ', $block->successors) . "]\n"; + $out .= " Dom: B{$block->dominator}\n"; + $out .= " DF: [" . implode(', ', $block->dominanceFrontier) . "]\n"; + + if (!empty($block->phi)) { + $out .= " Φ functions:\n"; + foreach ($block->phi as $varName => $phiVar) { + $sources = []; + foreach ($phiVar->phiSources as $srcBlock => $srcSsaId) { + $sources[] = "B{$srcBlock}→s{$srcSsaId}"; + } + $out .= " \${$varName} = φ(" . implode(', ', $sources) . ") → s{$phiVar->id}\n"; + } + } + + $out .= " Stmts (" . count($block->stmts) . "):\n"; + foreach ($block->stmts as $i => $stmt) { + $type = $stmt->getType(); + $defs = $this->getDefinedVars($stmt); + $defStr = !empty($defs) ? ' [defs: $' . implode(', $', $defs) . ']' : ''; + $out .= " [{$i}] {$type}{$defStr}\n"; + } + + if ($block->endsWithGoto) { + $out .= " Ends with goto {$block->gotoLabel}\n"; + } + $out .= "\n"; + } + + $out .= "--- SSA Variables ---\n"; + foreach ($this->ssaVars as $ssaVar) { + $flags = []; + if ($ssaVar->flags & SsaFlags::REFERENCE) $flags[] = 'REF'; + if ($ssaVar->flags & SsaFlags::UNDEFINED) $flags[] = 'UNDEF'; + if ($ssaVar->flags & SsaFlags::ESCAPED) $flags[] = 'ESC'; + if ($ssaVar->flags & SsaFlags::PHI) $flags[] = 'PHI'; + if ($ssaVar->flags & SsaFlags::PARAM) $flags[] = 'PARAM'; + if ($ssaVar->flags & SsaFlags::KILLED) $flags[] = 'KILLED'; + $flagStr = !empty($flags) ? ' [' . implode('|', $flags) . ']' : ''; + $out .= " s{$ssaVar->id} = \${$ssaVar->origName}{$flagStr}\n"; + } + + $out .= "\n--- Variable Def Blocks ---\n"; + foreach ($this->defBlocks as $varName => $blockIds) { + $out .= " \${$varName}: B" . implode(', B', $blockIds) . "\n"; + } + + return $out; + } +} diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 34a61df3..c9d138fe 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -9,6 +9,7 @@ namespace PhpAot\Php; use League\CLImate\CLImate; +use PhpAot\Php\Analysis\SsaBuilder; use PhpAot\Php\Backend\CompilerBackend; use PhpAot\Php\Backend\CompilerFactory; use PhpAot\Php\Context\FunctionContext; @@ -28,11 +29,13 @@ use PhpAot\Php\Generator\PlaceHolderGenerator; use PhpAot\Php\Generator\PropertyPromotion; use PhpAot\Php\Generator\Utils; use PhpAot\Php\Generator\TypeCheckGenerator; +use PhpAot\Php\Optimizer\SsaTypeOptimizer; use PhpAot\Php\Parser\StdContainerTrait; use PhpAot\Php\Parser\AssignOpTrait; use PhpAot\Php\Parser\BinaryOpTrait; use PhpAot\Php\Parser\TypeConversionTrait; use PhpAot\Php\Parser\TypeDetectionTrait; +use PhpAot\Php\Optimizer\FuncCallOptimizer; use PhpAot\Php\Platform\Linux; use PhpAot\Php\Platform\Macos; use PhpAot\Php\Platform\PlatformBase; @@ -71,6 +74,7 @@ class CompilerBase extends \PhpAot\Core\Translator use UniversalMethodCall; use Utils; use TypeCheckGenerator; + use SsaTypeOptimizer; public const string TYPE_VAR = 'php::Var'; public const string TYPE_BOOL = 'php::Bool'; @@ -531,9 +535,9 @@ class CompilerBase extends \PhpAot\Core\Translator $this->formatCppCode($file); } - public function isScalarInt(Expr $position): bool + public function isScalarInt(Expr $expr): bool { - return $position instanceof Node\Scalar\LNumber; + return $expr instanceof Node\Scalar\LNumber; } public function getLine($node): int @@ -1141,6 +1145,14 @@ class CompilerBase extends \PhpAot\Core\Translator } } + // Build SSA/e-SSA analysis for this function + if ($v->stmts) { + $this->context->ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList); + $this->context->ssaBuilder->build(); + // Narrow local variable types based on SSA analysis + $this->optimizeVarTypes(); + } + $stmts = ''; if ($v->stmts) { $this->indentLevel++; diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index d5554821..3dd64ce0 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -8,8 +8,13 @@ namespace PhpAot\Php\Context; +use PhpAot\Php\Analysis\SsaBuilder; + class FunctionContext { + /** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ + public ?SsaBuilder $ssaBuilder = null; + /** * @var array */ diff --git a/src/Php/FuncCallOptimizer.php b/src/Php/Optimizer/FuncCallOptimizer.php similarity index 99% rename from src/Php/FuncCallOptimizer.php rename to src/Php/Optimizer/FuncCallOptimizer.php index cde7fb36..e4232829 100644 --- a/src/Php/FuncCallOptimizer.php +++ b/src/Php/Optimizer/FuncCallOptimizer.php @@ -6,7 +6,7 @@ * @contact service@swoole.com */ -namespace PhpAot\Php; +namespace PhpAot\Php\Optimizer; use PhpParser\Node; diff --git a/src/Php/Optimizer/SsaTypeOptimizer.php b/src/Php/Optimizer/SsaTypeOptimizer.php new file mode 100644 index 00000000..1342066a --- /dev/null +++ b/src/Php/Optimizer/SsaTypeOptimizer.php @@ -0,0 +1,353 @@ +context->ssaBuilder; + if (!$ssa || empty($ssa->ssaVars)) { + return; + } + + $narrowableTypes = [ + self::TYPE_INT => true, + self::TYPE_FLOAT => true, + ]; + + // Group SSA vars by original variable name + $groups = []; + foreach ($ssa->ssaVars as $ssaVar) { + $name = $ssaVar->origName; + if (!isset($groups[$name])) { + $groups[$name] = []; + } + $groups[$name][] = $ssaVar; + } + + foreach ($groups as $varName => $varList) { + // Skip parameters — they already have declared types + if (isset($this->context->arguments[$varName])) { + continue; + } + + // Skip if any SSA var has dangerous flags + $hasPhi = false; + $hasDanger = false; + $narrowedType = null; + + foreach ($varList as $ssaVar) { + if ($ssaVar->flags & SsaFlags::PHI) { + $hasPhi = true; + continue; + } + if ($ssaVar->flags & (SsaFlags::REFERENCE | SsaFlags::ESCAPED | SsaFlags::KILLED)) { + $hasDanger = true; + break; + } + + $defType = $this->detectSsaDefType($ssaVar); + if ($defType === null || !isset($narrowableTypes[$defType])) { + $hasDanger = true; + break; + } + + if ($narrowedType === null) { + $narrowedType = $defType; + } elseif ($narrowedType !== $defType) { + $hasDanger = true; + break; + } + } + + if ($hasDanger || $narrowedType === null) { + continue; + } + + // Scan for operations that SSA definition types alone can't detect + $functionStmts = $this->context->ssaBuilder->getStmts(); + if ($functionStmts) { + if ($narrowedType === self::TYPE_INT && $this->hasDangerousIntOps($varName, $functionStmts)) { + continue; + } + if ($narrowedType === self::TYPE_FLOAT && $this->hasDangerousFloatOps($varName, $functionStmts)) { + continue; + } + } + + // Check φ-function sources for mixed types + if ($hasPhi && $this->phiSourcesHaveMixedTypes($ssa, $varList, $narrowedType)) { + continue; + } + + // Apply narrowing — bypass getNativeType(): SSA has PROVEN the type. + $this->context->localVars[$varName] = $narrowedType; + } + } + + /** + * Detect the type produced by a single SSA variable definition. + * Returns null if the type cannot be determined from the AST. + */ + private function detectSsaDefType(\PhpAot\Php\Analysis\SsaVar $ssaVar): ?string + { + $def = $ssaVar->definition; + if (!$def) { + return null; + } + + 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)) { + return null; + } + return $type; + } + + if ($def instanceof Node\Stmt\Foreach_) { + return null; + } + + if ($def instanceof Node\Stmt\Catch_) { + return self::TYPE_OBJECT; + } + + if ($def instanceof Node\Stmt\Static_) { + foreach ($def->vars as $staticVar) { + if ($staticVar->var->name === $ssaVar->origName && $staticVar->default) { + return $this->detectTypeOfExpr($staticVar->default); + } + } + return null; + } + + return null; + } + + /** + * Check whether a TYPE_INT expression involves +, -, *, ** operations + * that could overflow int64 at runtime and produce a float. + */ + private function exprCanOverflowInt(NodeAbstract $expr): bool + { + if ($expr instanceof Node\Expr\BinaryOp\Plus + || $expr instanceof Node\Expr\BinaryOp\Minus + || $expr instanceof Node\Expr\BinaryOp\Mul + || $expr instanceof Node\Expr\BinaryOp\Pow) { + $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, + $expr instanceof Node\Expr\BinaryOp\Pow => $leftConst ** $rightConst, + }; + return $result > PHP_INT_MAX || $result < PHP_INT_MIN; + } + + if ($this->isBoundaryConstant($expr->left) + || $this->isBoundaryConstant($expr->right)) { + return true; + } + + return false; + } + + if ($expr instanceof Node\Expr\FuncCall + && $expr->name instanceof Node\Name + && strtolower($expr->name->toString()) === 'pow') { + return true; + } + + if ($expr instanceof Node\Expr\BinaryOp) { + return $this->exprCanOverflowInt($expr->left) + || $this->exprCanOverflowInt($expr->right); + } + + return false; + } + + private function getIntConstantValue(NodeAbstract $node): ?int + { + if ($node instanceof Node\Scalar\LNumber) { + return $node->value; + } + return null; + } + + private function isBoundaryConstant(NodeAbstract $node): bool + { + if ($node instanceof Node\Expr\ConstFetch + && $node->name instanceof Node\Name) { + $name = $node->name->toLowerString(); + return $name === 'php_int_max' || $name === 'php_int_min'; + } + return false; + } + + private function phiSourcesHaveMixedTypes(SsaBuilder $ssa, array $varList, string $narrowedType): bool + { + foreach ($varList as $ssaVar) { + if (($ssaVar->flags & SsaFlags::PHI) && !empty($ssaVar->phiSources)) { + foreach ($ssaVar->phiSources as $srcSsaId) { + if (isset($ssa->ssaVars[$srcSsaId])) { + $srcVar = $ssa->ssaVars[$srcSsaId]; + $srcType = $this->detectSsaDefType($srcVar); + if ($srcType !== null && $srcType !== $narrowedType) { + return true; + } + } + } + } + } + return false; + } + + private function hasDangerousIntOps(string $varName, array $stmts): bool + { + foreach ($stmts as $stmt) { + if ($this->scanStmtForDangerousIntOps($stmt, $varName)) { + return true; + } + } + return false; + } + + private function scanStmtForDangerousIntOps($stmt, string $varName): bool + { + if (!$stmt instanceof Node) { + 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\Div) { + return true; + } + if ($stmt->expr instanceof Expr\AssignOp\Pow) { + return true; + } + if ($stmt->expr instanceof Expr\AssignOp\Plus + || $stmt->expr instanceof Expr\AssignOp\Minus + || $stmt->expr instanceof Expr\AssignOp\Mul + || $stmt->expr instanceof Expr\AssignOp\Mod + || $stmt->expr instanceof Expr\AssignOp\Concat + || $stmt->expr instanceof Expr\AssignOp\ShiftLeft + || $stmt->expr instanceof Expr\AssignOp\ShiftRight + || $stmt->expr instanceof Expr\AssignOp\BitwiseAnd + || $stmt->expr instanceof Expr\AssignOp\BitwiseOr + || $stmt->expr instanceof Expr\AssignOp\BitwiseXor) { + $rhsType = $this->detectTypeOfExpr($stmt->expr->expr); + if ($rhsType !== self::TYPE_INT) { + return true; + } + } + } + } + + return $this->recurseForDangerousOps($stmt, $varName, 'Int'); + } + + private function hasDangerousFloatOps(string $varName, array $stmts): bool + { + foreach ($stmts as $stmt) { + if ($this->scanStmtForDangerousFloatOps($stmt, $varName)) { + return true; + } + } + return false; + } + + private function scanStmtForDangerousFloatOps($stmt, string $varName): bool + { + if (!$stmt instanceof Node) { + 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) { + return true; + } + } + } + + return $this->recurseForDangerousOps($stmt, $varName, 'Float'); + } + + /** + * Recurse into compound statements (if/else, foreach, while, for, try/catch, switch). + */ + private function recurseForDangerousOps($stmt, string $varName, string $mode): bool + { + $method = 'hasDangerous' . $mode . 'Ops'; + + if ($stmt instanceof Node\Stmt\If_) { + if ($this->$method($varName, $stmt->stmts)) return true; + if (!empty($stmt->elseifs)) { + foreach ($stmt->elseifs as $elseif) { + if ($this->$method($varName, $elseif->stmts)) return true; + } + } + if ($stmt->else && $this->$method($varName, $stmt->else->stmts)) return true; + } + + if ($stmt instanceof Node\Stmt\Foreach_) { + if ($this->$method($varName, $stmt->stmts)) return true; + } + + if ($stmt instanceof Node\Stmt\While_ || $stmt instanceof Node\Stmt\Do_) { + if ($this->$method($varName, $stmt->stmts)) return true; + } + + if ($stmt instanceof Node\Stmt\For_) { + if ($this->$method($varName, $stmt->stmts)) return true; + } + + if ($stmt instanceof Node\Stmt\TryCatch) { + if ($this->$method($varName, $stmt->stmts)) return true; + foreach ($stmt->catches as $catch) { + if ($this->$method($varName, $catch->stmts)) return true; + } + if ($stmt->finally && $this->$method($varName, $stmt->finally->stmts)) return true; + } + + if ($stmt instanceof Node\Stmt\Case_ || $stmt instanceof Node\Stmt\Switch_) { + if (isset($stmt->stmts) && $this->$method($varName, $stmt->stmts)) return true; + } + + return false; + } +} diff --git a/tests/aot/optimizations/float-bitwise-mod.phpt b/tests/aot/optimizations/float-bitwise-mod.phpt new file mode 100644 index 00000000..a1e635ac --- /dev/null +++ b/tests/aot/optimizations/float-bitwise-mod.phpt @@ -0,0 +1,26 @@ +--TEST-- +SSA narrowing: bitwise/mod on float prevents narrowing +--FILE-- + +--EXPECT-- +int(1) +int(6) +float(6.28) diff --git a/tests/aot/optimizations/float-narrow-basic.phpt b/tests/aot/optimizations/float-narrow-basic.phpt new file mode 100644 index 00000000..581ea3ac --- /dev/null +++ b/tests/aot/optimizations/float-narrow-basic.phpt @@ -0,0 +1,26 @@ +--TEST-- +SSA narrowing: basic float operations +--FILE-- + +--EXPECT-- +float(3) +float(6.5) +float(3.5) diff --git a/tests/aot/optimizations/float-narrow-mixed.phpt b/tests/aot/optimizations/float-narrow-mixed.phpt new file mode 100644 index 00000000..77f24541 --- /dev/null +++ b/tests/aot/optimizations/float-narrow-mixed.phpt @@ -0,0 +1,26 @@ +--TEST-- +SSA narrowing: mixed int/float types stay Var +--FILE-- + +--EXPECT-- +float(3.14) +int(99) +int(45) diff --git a/tests/aot/optimizations/int-calculation.phpt b/tests/aot/optimizations/int-calculation.phpt new file mode 100644 index 00000000..114db63f --- /dev/null +++ b/tests/aot/optimizations/int-calculation.phpt @@ -0,0 +1,18 @@ +--TEST-- +SSA: int +--FILE-- + +--EXPECTF-- +12 +9.22%dE+%d \ No newline at end of file diff --git a/tests/aot/optimizations/int-narrow-basic.phpt b/tests/aot/optimizations/int-narrow-basic.phpt new file mode 100644 index 00000000..a1827c50 --- /dev/null +++ b/tests/aot/optimizations/int-narrow-basic.phpt @@ -0,0 +1,25 @@ +--TEST-- +SSA narrowing: basic int operations +--FILE-- + +--EXPECT-- +156 +2 +3 diff --git a/tests/aot/optimizations/int-narrow-overflow.phpt b/tests/aot/optimizations/int-narrow-overflow.phpt new file mode 100644 index 00000000..c155ec47 --- /dev/null +++ b/tests/aot/optimizations/int-narrow-overflow.phpt @@ -0,0 +1,24 @@ +--TEST-- +SSA narrowing: int overflow prevention (PHP_INT_MAX) +--FILE-- + +--EXPECT-- +double +double +int(101) diff --git a/tests/aot/optimizations/narrow-div-pow.phpt b/tests/aot/optimizations/narrow-div-pow.phpt new file mode 100644 index 00000000..0568513c --- /dev/null +++ b/tests/aot/optimizations/narrow-div-pow.phpt @@ -0,0 +1,25 @@ +--TEST-- +SSA narrowing: division and pow compound-assign prevent int narrowing +--FILE-- + +--EXPECT-- +float(3.3333333333333335) +int(8) +int(20) diff --git a/tests/aot/optimizations/narrow-ref-prevent.phpt b/tests/aot/optimizations/narrow-ref-prevent.phpt new file mode 100644 index 00000000..45b7b721 --- /dev/null +++ b/tests/aot/optimizations/narrow-ref-prevent.phpt @@ -0,0 +1,19 @@ +--TEST-- +SSA narrowing: reference assignment prevents narrowing +--FILE-- + +--EXPECT-- +200 +75