- 添加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