feat(compiler): 实现SSA分析和类型优化功能

- 添加SsaBuilder类用于构建SSA形式和e-SSA分析
- 引入SsaTypeOptimizer trait实现基于SSA的类型窄化优化
- 将FuncCallOptimizer移至Optimizer命名空间
- 在FunctionContext中添加SSA分析器实例
- 实现变量类型的自动窄化为C++原生类型(int/float)
- 添加防止危险操作的类型安全检查
- 支持常量折叠和溢出检测逻辑
- 重构函数参数名称以提高代码可读性
- 添加完整的SSA构建器单元测试覆盖各种场景
pull/1/head
韩天峰 3 months ago
parent d377777571
commit bf31da8e7b
  1. 472
      phpunit/src/Analysis/SsaBuilderTest.php
  2. 1694
      src/Php/Analysis/SsaBuilder.php
  3. 16
      src/Php/CompilerBase.php
  4. 5
      src/Php/Context/FunctionContext.php
  5. 2
      src/Php/Optimizer/FuncCallOptimizer.php
  6. 353
      src/Php/Optimizer/SsaTypeOptimizer.php
  7. 26
      tests/aot/optimizations/float-bitwise-mod.phpt
  8. 26
      tests/aot/optimizations/float-narrow-basic.phpt
  9. 26
      tests/aot/optimizations/float-narrow-mixed.phpt
  10. 18
      tests/aot/optimizations/int-calculation.phpt
  11. 25
      tests/aot/optimizations/int-narrow-basic.phpt
  12. 24
      tests/aot/optimizations/int-narrow-overflow.phpt
  13. 25
      tests/aot/optimizations/narrow-div-pow.phpt
  14. 19
      tests/aot/optimizations/narrow-ref-prevent.phpt

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

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

@ -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<string, string>
*/

@ -6,7 +6,7 @@
* @contact service@swoole.com
*/
namespace PhpAot\Php;
namespace PhpAot\Php\Optimizer;
use PhpParser\Node;

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