- 添加SsaBuilder类用于构建SSA形式和e-SSA分析 - 引入SsaTypeOptimizer trait实现基于SSA的类型窄化优化 - 将FuncCallOptimizer移至Optimizer命名空间 - 在FunctionContext中添加SSA分析器实例 - 实现变量类型的自动窄化为C++原生类型(int/float) - 添加防止危险操作的类型安全检查 - 支持常量折叠和溢出检测逻辑 - 重构函数参数名称以提高代码可读性 - 添加完整的SSA构建器单元测试覆盖各种场景pull/1/head
parent
d377777571
commit
bf31da8e7b
14 changed files with 2728 additions and 3 deletions
@ -0,0 +1,472 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace PhpAot\Tests\Analysis; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use PhpAot\Php\Analysis\SsaBlock; |
||||||
|
use PhpAot\Php\Analysis\SsaBuilder; |
||||||
|
use PhpAot\Php\Analysis\SsaFlags; |
||||||
|
use PhpAot\Php\Analysis\SsaVar; |
||||||
|
use PhpParser\Node\Expr; |
||||||
|
use PhpParser\Node\FunctionLike; |
||||||
|
use PhpParser\Node\Stmt; |
||||||
|
use PhpParser\ParserFactory; |
||||||
|
|
||||||
|
class SsaBuilderTest extends TestCase |
||||||
|
{ |
||||||
|
private function parsePhp(string $code): array |
||||||
|
{ |
||||||
|
$parser = (new ParserFactory())->createForHostVersion(); |
||||||
|
$stmts = $parser->parse('<?php ' . $code); |
||||||
|
$this->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'); |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,353 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* SSA-based type narrowing optimizer. |
||||||
|
* |
||||||
|
* Uses SSA/e-SSA analysis to narrow local variable types from php::Var |
||||||
|
* to C++ native types (php::Int, php::Float) when all definitions |
||||||
|
* provably produce the same type and no dangerous operations exist. |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace PhpAot\Php\Optimizer; |
||||||
|
|
||||||
|
use PhpAot\Php\Analysis\SsaBuilder; |
||||||
|
use PhpAot\Php\Analysis\SsaFlags; |
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\Node\Expr; |
||||||
|
use PhpParser\NodeAbstract; |
||||||
|
|
||||||
|
trait SsaTypeOptimizer |
||||||
|
{ |
||||||
|
/** |
||||||
|
* Use SSA analysis to narrow local variable types to C++ native types. |
||||||
|
* |
||||||
|
* For each variable, if ALL definitions produce the same narrow type |
||||||
|
* (int, float), and the variable is never referenced, escaped, |
||||||
|
* killed, or defined by a φ function with mixed sources, pre-set the |
||||||
|
* type in localVars so genScopeVarDecl emits the narrow C++ type. |
||||||
|
*/ |
||||||
|
protected function optimizeVarTypes(): void |
||||||
|
{ |
||||||
|
$ssa = $this->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; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA narrowing: bitwise/mod on float prevents narrowing |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
// %= on float: PHP converts to int → $a stays Var |
||||||
|
$a = 10.5; |
||||||
|
$a %= 3; |
||||||
|
var_dump($a); |
||||||
|
|
||||||
|
// |= on float: PHP converts to int → $b stays Var |
||||||
|
$b = 6.7; |
||||||
|
$b |= 2; |
||||||
|
var_dump($b); |
||||||
|
|
||||||
|
// Pure float arithmetic → narrowed to Float |
||||||
|
$c = 3.0; |
||||||
|
$c += 0.14; |
||||||
|
$c *= 2.0; |
||||||
|
var_dump($c); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(1) |
||||||
|
int(6) |
||||||
|
float(6.28) |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA narrowing: basic float operations |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
// 1.0 + 0.5 = 1.5, * 2 = 3.0 (all dyadic fractions, no precision loss) |
||||||
|
$a = 1.0; |
||||||
|
$a += 0.5; |
||||||
|
$a *= 2.0; |
||||||
|
var_dump($a); |
||||||
|
|
||||||
|
// 10.0 - 3.5 = 6.5 (dyadic) |
||||||
|
$b = 10.0; |
||||||
|
$b -= 3.5; |
||||||
|
var_dump($b); |
||||||
|
|
||||||
|
// 2.5 + 1 = 3.5 (dyadic) |
||||||
|
$c = 2.5; |
||||||
|
$c++; |
||||||
|
var_dump($c); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
float(3) |
||||||
|
float(6.5) |
||||||
|
float(3.5) |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA narrowing: mixed int/float types stay Var |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
// Mixed definitions: int then float → stays Var |
||||||
|
$a = 10; |
||||||
|
$a = 3.14; |
||||||
|
var_dump($a); |
||||||
|
|
||||||
|
// Float then int → stays Var |
||||||
|
$b = 2.5; |
||||||
|
$b = 99; |
||||||
|
var_dump($b); |
||||||
|
|
||||||
|
// Int compound assigns, all with int RHS → narrowed to int |
||||||
|
$c = 10; |
||||||
|
$c += 5; |
||||||
|
$c *= 3; |
||||||
|
var_dump($c); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
float(3.14) |
||||||
|
int(99) |
||||||
|
int(45) |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA: int |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
$a = 100; |
||||||
|
$a += 3; |
||||||
|
$a *= 232; |
||||||
|
$a %= 3412; |
||||||
|
echo $a, PHP_EOL; |
||||||
|
|
||||||
|
$b = $a + PHP_INT_MAX; |
||||||
|
echo $b, PHP_EOL; |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECTF-- |
||||||
|
12 |
||||||
|
9.22%dE+%d |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA narrowing: basic int operations |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
$a = 100; |
||||||
|
$a += 3; |
||||||
|
$a *= 2; |
||||||
|
$a -= 50; |
||||||
|
echo $a, PHP_EOL; |
||||||
|
|
||||||
|
$b = 42; |
||||||
|
$b %= 10; |
||||||
|
echo $b, PHP_EOL; |
||||||
|
|
||||||
|
$c = 1; |
||||||
|
$c++; |
||||||
|
$c++; |
||||||
|
echo $c, PHP_EOL; |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
156 |
||||||
|
2 |
||||||
|
3 |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA narrowing: int overflow prevention (PHP_INT_MAX) |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
$a = 2; |
||||||
|
|
||||||
|
// $a + PHP_INT_MAX overflows to float at runtime |
||||||
|
$b = $a + PHP_INT_MAX; |
||||||
|
echo gettype($b), PHP_EOL; |
||||||
|
|
||||||
|
// $a * PHP_INT_MAX also overflows |
||||||
|
$c = $a * PHP_INT_MAX; |
||||||
|
echo gettype($c), PHP_EOL; |
||||||
|
|
||||||
|
// Two small ints added safely → narrowed to int |
||||||
|
$d = $a + 99; |
||||||
|
var_dump($d); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
double |
||||||
|
double |
||||||
|
int(101) |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA narrowing: division and pow compound-assign prevent int narrowing |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
// /= prevents int narrowing → $a stays Var (float result in PHP) |
||||||
|
$a = 10; |
||||||
|
$a /= 3; |
||||||
|
var_dump($a); |
||||||
|
|
||||||
|
// **= prevents int narrowing → $b stays Var |
||||||
|
$b = 2; |
||||||
|
$b **= 3; |
||||||
|
var_dump($b); |
||||||
|
|
||||||
|
// *= with int RHS is safe → narrowed to int |
||||||
|
$c = 5; |
||||||
|
$c *= 4; |
||||||
|
var_dump($c); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
float(3.3333333333333335) |
||||||
|
int(8) |
||||||
|
int(20) |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
--TEST-- |
||||||
|
SSA narrowing: reference assignment prevents narrowing |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main(): void { |
||||||
|
$a = 100; |
||||||
|
$b = &$a; |
||||||
|
$a = 200; |
||||||
|
echo $b, PHP_EOL; |
||||||
|
|
||||||
|
// $c not referenced → can be narrowed |
||||||
|
$c = 50; |
||||||
|
$c += 25; |
||||||
|
echo $c, PHP_EOL; |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
200 |
||||||
|
75 |
||||||
Loading…
Reference in new issue