TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
554 lines
18 KiB
554 lines
18 KiB
<?php
|
|
|
|
namespace TypePhp\Tests\Analysis;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use TypePhp\Analysis\SsaBlock;
|
|
use TypePhp\Analysis\SsaBuilder;
|
|
use TypePhp\Analysis\SsaFlags;
|
|
use TypePhp\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 testAssignOpAndIncDecCreateNewSsaVars(): void
|
|
{
|
|
$builder = $this->buildSsa('$y = 1; $y &= 3; $y++; --$y;');
|
|
$yVars = [];
|
|
foreach ($builder->ssaVars as $var) {
|
|
if ($var->origName === 'y' && !($var->flags & SsaFlags::PHI)) {
|
|
$yVars[] = $var;
|
|
}
|
|
}
|
|
|
|
$this->assertCount(4, $yVars, 'Assignment, compound assignment, and inc/dec should each define $y');
|
|
}
|
|
|
|
public function testVarDefBlocks(): void
|
|
{
|
|
$builder = $this->buildSsa('$a = 1; $b = 2;');
|
|
$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 testImmediateDominatorUsesClosestDominator(): void
|
|
{
|
|
$builder = $this->buildSsa('
|
|
$a = 1;
|
|
target:
|
|
$b = 2;
|
|
');
|
|
|
|
$exitBlock = $builder->blocks[count($builder->blocks) - 1];
|
|
$labelBlockId = $builder->getLabelBlock('target');
|
|
|
|
$this->assertNotNull($labelBlockId);
|
|
$this->assertSame($labelBlockId, $exitBlock->dominator);
|
|
}
|
|
|
|
public function testPhiFunctionPlacedAtJoin(): void
|
|
{
|
|
$builder = $this->buildSsa('
|
|
$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 testNestedCallByRefCreatesEscapedVar(): void
|
|
{
|
|
$builder = $this->buildSsa('
|
|
$y = some_func(refval($x));
|
|
');
|
|
|
|
$escapedVar = null;
|
|
foreach ($builder->ssaVars as $var) {
|
|
if ($var->origName === 'x' && ($var->flags & SsaFlags::ESCAPED)) {
|
|
$escapedVar = $var;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertNotNull($escapedVar, 'Nested refval($x) should create ESCAPED SSA var');
|
|
}
|
|
|
|
public function testCallByRefInsideLoopCreatesEscapedVar(): void
|
|
{
|
|
$builder = $this->buildSsa('
|
|
while ($x) {
|
|
some_func(refval($x));
|
|
}
|
|
');
|
|
|
|
$escapedVar = null;
|
|
foreach ($builder->ssaVars as $var) {
|
|
if ($var->origName === 'x' && ($var->flags & SsaFlags::ESCAPED)) {
|
|
$escapedVar = $var;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertNotNull($escapedVar, 'refval($x) inside loop body should create ESCAPED SSA var');
|
|
}
|
|
|
|
public function testRefvalCallByRefCreatesEscapedVar(): void
|
|
{
|
|
// refval() is the AOT compiler's pseudo-function for dynamic call reference passing
|
|
$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 testLoopBodyAssignmentsCreateSsaVars(): void
|
|
{
|
|
$builder = $this->buildSsa('
|
|
$obj = new Foo();
|
|
while ($x) {
|
|
$obj = new Foo();
|
|
}
|
|
');
|
|
|
|
$objVars = [];
|
|
foreach ($builder->ssaVars as $var) {
|
|
if ($var->origName === 'obj' && !($var->flags & SsaFlags::PHI)) {
|
|
$objVars[] = $var;
|
|
}
|
|
}
|
|
|
|
$this->assertCount(2, $objVars, 'Assignment inside loop body should be tracked as an SSA definition');
|
|
$this->assertContains(0, $builder->getDefBlocks('obj'));
|
|
}
|
|
|
|
public function testStaticVariableIsEscaped(): void
|
|
{
|
|
$builder = $this->buildSsa('
|
|
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');
|
|
}
|
|
}
|
|
|