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.
1596 lines
59 KiB
1596 lines
59 KiB
<?php
|
|
|
|
namespace TypePhp\Tests;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use TypePhp\Analysis\SsaBuilder;
|
|
use TypePhp\Analysis\SsaFlags;
|
|
use TypePhp\Analysis\SsaVar;
|
|
use TypePhp\Analysis\SsaBlock;
|
|
use TypePhp\Analysis\VarState;
|
|
use TypePhp\Analysis\PiConstraint;
|
|
use TypePhp\CompilerBase;
|
|
use TypePhp\CompilerTest;
|
|
use TypePhp\Entity\ClassDef;
|
|
use TypePhp\Entity\MethodDef;
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\FunctionLike;
|
|
use PhpParser\Node\Stmt;
|
|
use PhpParser\Node\Arg;
|
|
use PhpParser\Node\Scalar;
|
|
use PhpParser\ParserFactory;
|
|
|
|
class SsaAnalysisTest extends TestCase
|
|
{
|
|
private CompilerTest $compiler;
|
|
private \ReflectionClass $ref;
|
|
private string $tmpDir;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
// SsaFlags, SsaVar, SsaBlock, etc. are defined in SsaBuilder.php
|
|
// but PSR-4 expects each class in its own file. Load them early.
|
|
require_once __DIR__ . '/../../src/Analysis/SsaBuilder.php';
|
|
$this->tmpDir = sys_get_temp_dir() . '/ssa_test_' . uniqid();
|
|
mkdir($this->tmpDir, 0777, true);
|
|
$this->compiler = CompilerTest::create($this->tmpDir);
|
|
$this->ref = new \ReflectionClass($this->compiler);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
parent::tearDown();
|
|
if (is_dir($this->tmpDir)) {
|
|
$this->removeDirectory($this->tmpDir);
|
|
}
|
|
}
|
|
|
|
private function removeDirectory(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
$files = array_diff(scandir($dir), ['.', '..']);
|
|
foreach ($files as $file) {
|
|
$path = $dir . DIRECTORY_SEPARATOR . $file;
|
|
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
|
|
private function invoke(string $method, ...$args): mixed
|
|
{
|
|
$m = $this->ref->getMethod($method);
|
|
$m->setAccessible(true);
|
|
return $m->invoke($this->compiler, ...$args);
|
|
}
|
|
|
|
private function setProperty(string $name, mixed $value): void
|
|
{
|
|
$prop = $this->ref->getProperty($name);
|
|
$prop->setAccessible(true);
|
|
$prop->setValue($this->compiler, $value);
|
|
}
|
|
|
|
private function getContextProperty(string $name): mixed
|
|
{
|
|
$ctxProp = $this->ref->getProperty('context');
|
|
$ctxProp->setAccessible(true);
|
|
$context = $ctxProp->getValue($this->compiler);
|
|
$prop = new \ReflectionProperty($context, $name);
|
|
$prop->setAccessible(true);
|
|
return $prop->getValue($context);
|
|
}
|
|
|
|
private function setContextProperty(string $name, mixed $value): void
|
|
{
|
|
$ctxProp = $this->ref->getProperty('context');
|
|
$ctxProp->setAccessible(true);
|
|
$context = $ctxProp->getValue($this->compiler);
|
|
$prop = new \ReflectionProperty($context, $name);
|
|
$prop->setAccessible(true);
|
|
$prop->setValue($context, $value);
|
|
}
|
|
|
|
private function optimizeLoopVarsForCode(string $code): array
|
|
{
|
|
$parser = (new ParserFactory())->createForHostVersion();
|
|
$stmts = $parser->parse('<?php function f($s = "") { ' . $code . ' }');
|
|
$this->assertNotNull($stmts);
|
|
|
|
$fn = $stmts[0];
|
|
$this->assertInstanceOf(FunctionLike::class, $fn);
|
|
|
|
$this->invoke('resetFunction');
|
|
$builder = new SsaBuilder($fn->getStmts() ?: [], []);
|
|
$builder->build();
|
|
$this->setContextProperty('ssaBuilder', $builder);
|
|
|
|
$this->invoke('optimizeLoopVars', $builder);
|
|
|
|
return $this->getContextProperty('localVars');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaFlags: constant values
|
|
// ========================================================================
|
|
|
|
public function testSsaFlagsAreDistinct(): void
|
|
{
|
|
$flags = [
|
|
SsaFlags::UNDEFINED,
|
|
SsaFlags::REFERENCE,
|
|
SsaFlags::ESCAPED,
|
|
SsaFlags::PHI,
|
|
SsaFlags::PARAM,
|
|
SsaFlags::KILLED,
|
|
];
|
|
$this->assertCount(6, array_unique($flags), 'All SSA flags must be distinct powers of 2');
|
|
}
|
|
|
|
public function testSsaFlagsArePowersOfTwo(): void
|
|
{
|
|
$flags = [
|
|
SsaFlags::UNDEFINED,
|
|
SsaFlags::REFERENCE,
|
|
SsaFlags::ESCAPED,
|
|
SsaFlags::PHI,
|
|
SsaFlags::PARAM,
|
|
SsaFlags::KILLED,
|
|
];
|
|
foreach ($flags as $flag) {
|
|
$this->assertGreaterThan(0, $flag);
|
|
$this->assertEquals(0, $flag & ($flag - 1), "Flag {$flag} must be a power of 2");
|
|
}
|
|
}
|
|
|
|
public function testSsaFlagValues(): void
|
|
{
|
|
$this->assertEquals(1, SsaFlags::UNDEFINED);
|
|
$this->assertEquals(2, SsaFlags::REFERENCE);
|
|
$this->assertEquals(4, SsaFlags::ESCAPED);
|
|
$this->assertEquals(8, SsaFlags::PHI);
|
|
$this->assertEquals(16, SsaFlags::PARAM);
|
|
$this->assertEquals(32, SsaFlags::KILLED);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaVar
|
|
// ========================================================================
|
|
|
|
public function testSsaVarCreation(): void
|
|
{
|
|
$var = new SsaVar(42, 'myVar', SsaFlags::PARAM);
|
|
$this->assertEquals(42, $var->id);
|
|
$this->assertEquals('myVar', $var->origName);
|
|
$this->assertEquals(SsaFlags::PARAM, $var->flags);
|
|
$this->assertNull($var->definition);
|
|
$this->assertNull($var->pi);
|
|
$this->assertEmpty($var->phiSources);
|
|
$this->assertFalse($var->isConstant);
|
|
$this->assertNull($var->constantValue);
|
|
}
|
|
|
|
public function testSsaVarDefaults(): void
|
|
{
|
|
$var = new SsaVar(1, 'x');
|
|
$this->assertEquals(0, $var->flags);
|
|
$this->assertNull($var->definition);
|
|
}
|
|
|
|
public function testSsaVarFlagsCombinable(): void
|
|
{
|
|
$flags = SsaFlags::REFERENCE | SsaFlags::ESCAPED;
|
|
$var = new SsaVar(1, 'x', $flags);
|
|
$this->assertTrue(($var->flags & SsaFlags::REFERENCE) !== 0);
|
|
$this->assertTrue(($var->flags & SsaFlags::ESCAPED) !== 0);
|
|
$this->assertFalse(($var->flags & SsaFlags::PHI) !== 0);
|
|
}
|
|
|
|
// ========================================================================
|
|
// PiConstraint
|
|
// ========================================================================
|
|
|
|
public function testPiConstraintDefaults(): void
|
|
{
|
|
$pi = new PiConstraint();
|
|
$this->assertEquals('', $pi->narrowedType);
|
|
$this->assertNull($pi->condition);
|
|
$this->assertTrue($pi->isInstanceof);
|
|
$this->assertFalse($pi->hasRange);
|
|
$this->assertEquals(0, $pi->rangeMin);
|
|
$this->assertEquals(0, $pi->rangeMax);
|
|
}
|
|
|
|
public function testPiConstraintCustom(): void
|
|
{
|
|
$pi = new PiConstraint();
|
|
$pi->narrowedType = 'MyClass';
|
|
$pi->isInstanceof = false;
|
|
$pi->hasRange = true;
|
|
$pi->rangeMin = 1;
|
|
$pi->rangeMax = 10;
|
|
|
|
$this->assertEquals('MyClass', $pi->narrowedType);
|
|
$this->assertFalse($pi->isInstanceof);
|
|
$this->assertTrue($pi->hasRange);
|
|
$this->assertEquals(1, $pi->rangeMin);
|
|
$this->assertEquals(10, $pi->rangeMax);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaBlock
|
|
// ========================================================================
|
|
|
|
public function testSsaBlockDefaults(): void
|
|
{
|
|
$block = new SsaBlock();
|
|
$this->assertEmpty($block->stmts);
|
|
$this->assertEmpty($block->predecessors);
|
|
$this->assertEmpty($block->successors);
|
|
$this->assertFalse($block->isGotoTarget);
|
|
$this->assertNull($block->labelName);
|
|
$this->assertFalse($block->endsWithGoto);
|
|
$this->assertNull($block->gotoLabel);
|
|
$this->assertEquals(-1, $block->branchTrueBlock);
|
|
$this->assertEquals(-1, $block->branchFalseBlock);
|
|
$this->assertEquals(-1, $block->branchJoinBlock);
|
|
$this->assertFalse($block->isJoinPoint);
|
|
$this->assertEquals(-1, $block->forceJumpTo);
|
|
$this->assertEmpty($block->phi);
|
|
$this->assertEquals(-1, $block->dominator);
|
|
$this->assertEmpty($block->dominatedChildren);
|
|
$this->assertEmpty($block->dominanceFrontier);
|
|
}
|
|
|
|
// ========================================================================
|
|
// VarState
|
|
// ========================================================================
|
|
|
|
public function testVarStateDefaults(): void
|
|
{
|
|
$state = new VarState();
|
|
$this->assertEmpty($state->stack);
|
|
$this->assertEquals(0, $state->counter);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaBuilder: basic construction
|
|
// ========================================================================
|
|
|
|
public function testSsaBuilderEmptyFunction(): void
|
|
{
|
|
$builder = new SsaBuilder([]);
|
|
$builder->build();
|
|
|
|
$this->assertGreaterThanOrEqual(2, count($builder->blocks), 'Should have at least entry + exit blocks');
|
|
$this->assertEmpty($builder->ssaVars);
|
|
}
|
|
|
|
public function testSsaBuilderSimpleAssignment(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$val = new Scalar\LNumber(42);
|
|
$assign = new Stmt\Expression(new Expr\Assign($var, $val));
|
|
|
|
$builder = new SsaBuilder([$assign]);
|
|
$builder->build();
|
|
|
|
$this->assertGreaterThan(0, count($builder->ssaVars), 'Should create SSA var for assignment');
|
|
}
|
|
|
|
public function testSsaBuilderWithParams(): void
|
|
{
|
|
$argInfoList = [
|
|
(object) ['name' => 'a', 'byRef' => false],
|
|
(object) ['name' => 'b', 'byRef' => true],
|
|
];
|
|
|
|
$builder = new SsaBuilder([], $argInfoList);
|
|
$builder->build();
|
|
|
|
$paramVars = array_filter($builder->ssaVars, fn($v) => ($v->flags & SsaFlags::PARAM) !== 0);
|
|
$this->assertCount(2, $paramVars, 'Two parameters should create 2 PARAM SSA vars');
|
|
|
|
$bVars = array_filter($paramVars, fn($v) => $v->origName === 'b');
|
|
$bVar = reset($bVars);
|
|
$this->assertNotFalse($bVar);
|
|
$this->assertTrue(($bVar->flags & SsaFlags::REFERENCE) !== 0, 'byRef param should have REFERENCE flag');
|
|
}
|
|
|
|
public function testSsaBuilderGetStmts(): void
|
|
{
|
|
$stmts = [new Stmt\Expression(new Expr\Assign(new Expr\Variable('x'), new Scalar\LNumber(1)))];
|
|
$builder = new SsaBuilder($stmts);
|
|
$this->assertSame($stmts, $builder->getStmts());
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaBuilder: CFG with control flow
|
|
// ========================================================================
|
|
|
|
public function testSsaBuilderIfElseCreatesBlocks(): void
|
|
{
|
|
$cond = new Expr\Variable('cond');
|
|
$trueAssign = new Stmt\Expression(new Expr\Assign(new Expr\Variable('x'), new Scalar\LNumber(1)));
|
|
$falseAssign = new Stmt\Expression(new Expr\Assign(new Expr\Variable('x'), new Scalar\LNumber(2)));
|
|
$ifStmt = new Stmt\If_($cond, [
|
|
'stmts' => [$trueAssign],
|
|
'elseifs' => [],
|
|
'else' => new Stmt\Else_([$falseAssign]),
|
|
]);
|
|
|
|
$builder = new SsaBuilder([$ifStmt]);
|
|
$builder->build();
|
|
|
|
$this->assertGreaterThan(4, count($builder->blocks), 'If-else should create multiple blocks');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaBuilder: goto and labels
|
|
// ========================================================================
|
|
|
|
public function testSsaBuilderGotoLabel(): void
|
|
{
|
|
$label = new Stmt\Label('target');
|
|
$label->name = new Node\Identifier('target');
|
|
$gotoStmt = new Stmt\Goto_('target');
|
|
$gotoStmt->name = new Node\Identifier('target');
|
|
$assign = new Stmt\Expression(new Expr\Assign(new Expr\Variable('x'), new Scalar\LNumber(1)));
|
|
|
|
$builder = new SsaBuilder([$assign, $gotoStmt, $label, $assign]);
|
|
$builder->build();
|
|
|
|
$gotoBlocks = array_filter($builder->blocks, fn($b) => $b->endsWithGoto);
|
|
$labelBlocks = array_filter($builder->blocks, fn($b) => $b->isGotoTarget);
|
|
|
|
$this->assertCount(1, $gotoBlocks, 'Should have one block ending with goto');
|
|
$this->assertCount(1, $labelBlocks, 'Should have one label block');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaBuilder: getDefBlocks
|
|
// ========================================================================
|
|
|
|
public function testSsaBuilderGetDefBlocks(): void
|
|
{
|
|
$assign1 = new Stmt\Expression(new Expr\Assign(new Expr\Variable('x'), new Scalar\LNumber(1)));
|
|
$assign2 = new Stmt\Expression(new Expr\Assign(new Expr\Variable('y'), new Scalar\LNumber(2)));
|
|
|
|
$builder = new SsaBuilder([$assign1, $assign2]);
|
|
$builder->build();
|
|
|
|
$xBlocks = $builder->getDefBlocks('x');
|
|
$yBlocks = $builder->getDefBlocks('y');
|
|
|
|
$this->assertNotEmpty($xBlocks, 'x should have def blocks');
|
|
$this->assertNotEmpty($yBlocks, 'y should have def blocks');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaBuilder: buildPiConstraints
|
|
// ========================================================================
|
|
|
|
public function testBuildPiConstraintsInstanceof(): void
|
|
{
|
|
$var = new Expr\Variable('obj');
|
|
$class = new Node\Name('MyClass');
|
|
$instanceof = new Expr\Instanceof_($var, $class);
|
|
$ifStmt = new Stmt\If_($instanceof, ['stmts' => [], 'elseifs' => [], 'else' => null]);
|
|
|
|
$builder = new SsaBuilder([]);
|
|
$result = $builder->buildPiConstraints($ifStmt);
|
|
|
|
$this->assertArrayHasKey('obj', $result['trueVars']);
|
|
$this->assertArrayHasKey('obj', $result['falseVars']);
|
|
|
|
$this->assertEquals('MyClass', $result['trueVars']['obj']->narrowedType);
|
|
$this->assertTrue($result['trueVars']['obj']->isInstanceof);
|
|
|
|
$this->assertEquals('!MyClass', $result['falseVars']['obj']->narrowedType);
|
|
$this->assertFalse($result['falseVars']['obj']->isInstanceof);
|
|
}
|
|
|
|
public function testBuildPiConstraintsIsInt(): void
|
|
{
|
|
$arg = new Arg(new Expr\Variable('x'));
|
|
$funcCall = new Expr\FuncCall(new Node\Name('is_int'), [$arg]);
|
|
$ifStmt = new Stmt\If_($funcCall, ['stmts' => [], 'elseifs' => [], 'else' => null]);
|
|
|
|
$builder = new SsaBuilder([]);
|
|
$result = $builder->buildPiConstraints($ifStmt);
|
|
|
|
$this->assertArrayHasKey('x', $result['trueVars']);
|
|
$this->assertEquals('int', $result['trueVars']['x']->narrowedType);
|
|
$this->assertFalse($result['trueVars']['x']->isInstanceof);
|
|
}
|
|
|
|
public function testBuildPiConstraintsNegation(): void
|
|
{
|
|
$var = new Expr\Variable('obj');
|
|
$class = new Node\Name('MyClass');
|
|
$instanceof = new Expr\Instanceof_($var, $class);
|
|
$negated = new Expr\BooleanNot($instanceof);
|
|
$ifStmt = new Stmt\If_($negated, ['stmts' => [], 'elseifs' => [], 'else' => null]);
|
|
|
|
$builder = new SsaBuilder([]);
|
|
$result = $builder->buildPiConstraints($ifStmt);
|
|
|
|
$this->assertArrayHasKey('obj', $result['trueVars']);
|
|
$this->assertEquals('!MyClass', $result['trueVars']['obj']->narrowedType);
|
|
}
|
|
|
|
public function testBuildPiConstraintsPlainVariable(): void
|
|
{
|
|
$cond = new Expr\Variable('flag');
|
|
$ifStmt = new Stmt\If_($cond, ['stmts' => [], 'elseifs' => [], 'else' => null]);
|
|
|
|
$builder = new SsaBuilder([]);
|
|
$result = $builder->buildPiConstraints($ifStmt);
|
|
|
|
$this->assertEmpty($result['trueVars']);
|
|
$this->assertEmpty($result['falseVars']);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaBuilder: dump
|
|
// ========================================================================
|
|
|
|
public function testSsaBuilderDump(): void
|
|
{
|
|
$assign = new Stmt\Expression(new Expr\Assign(new Expr\Variable('x'), new Scalar\LNumber(42)));
|
|
$builder = new SsaBuilder([$assign]);
|
|
$builder->build();
|
|
|
|
$dump = $builder->dump();
|
|
$this->assertStringContainsString('SSA Builder Dump', $dump);
|
|
$this->assertStringContainsString('Blocks:', $dump);
|
|
$this->assertStringContainsString('SSA Vars:', $dump);
|
|
$this->assertStringContainsString('$x', $dump);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaPropOptimizer: resolveNewExprClass
|
|
// ========================================================================
|
|
|
|
public function testResolveNewExprClassFullyQualified(): void
|
|
{
|
|
$name = new Node\Name\FullyQualified('App\\Service\\User');
|
|
$newExpr = new Expr\New_($name);
|
|
|
|
$result = $this->invoke('resolveNewExprClass', $newExpr);
|
|
$this->assertEquals('App\\Service\\User', $result);
|
|
}
|
|
|
|
public function testResolveNewExprClassUnqualified(): void
|
|
{
|
|
$name = new Node\Name('MyClass');
|
|
$newExpr = new Expr\New_($name);
|
|
|
|
$this->setProperty('namespace', 'App\\Service');
|
|
|
|
$result = $this->invoke('resolveNewExprClass', $newExpr);
|
|
$this->assertEquals('App\\Service\\MyClass', $result);
|
|
}
|
|
|
|
public function testResolveNewExprClassSelf(): void
|
|
{
|
|
$name = new Node\Name('self');
|
|
$newExpr = new Expr\New_($name);
|
|
|
|
$classDef = new ClassDef('MyService', 0, 'App\\Service');
|
|
$this->setProperty('classDef', $classDef);
|
|
$this->setProperty('namespace', 'App\\Service');
|
|
$this->setProperty('class', 'MyService');
|
|
|
|
$result = $this->invoke('resolveNewExprClass', $newExpr);
|
|
$this->assertEquals('App\\Service\\MyService', $result);
|
|
}
|
|
|
|
public function testResolveNewExprClassStatic(): void
|
|
{
|
|
$name = new Node\Name('static');
|
|
$newExpr = new Expr\New_($name);
|
|
|
|
$classDef = new ClassDef('MyService', 0, 'App\\Service');
|
|
$this->setProperty('classDef', $classDef);
|
|
$this->setProperty('namespace', 'App\\Service');
|
|
$this->setProperty('class', 'MyService');
|
|
|
|
$result = $this->invoke('resolveNewExprClass', $newExpr);
|
|
$this->assertNull($result, 'static cannot be resolved at compile time (late static binding)');
|
|
}
|
|
|
|
public function testResolveNewExprClassSelfNoClassDef(): void
|
|
{
|
|
$name = new Node\Name('self');
|
|
$newExpr = new Expr\New_($name);
|
|
|
|
$result = $this->invoke('resolveNewExprClass', $newExpr);
|
|
$this->assertNull($result, 'Should return null when classDef is not set');
|
|
}
|
|
|
|
public function testResolveNewExprClassNotNew(): void
|
|
{
|
|
$var = new Expr\Variable('obj');
|
|
$result = $this->invoke('resolveNewExprClass', $var);
|
|
$this->assertNull($result, 'Non-New_ expressions should return null');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaPropOptimizer: isPropOfObj
|
|
// ========================================================================
|
|
|
|
public function testIsPropOfObjTrue(): void
|
|
{
|
|
$objVar = new Expr\Variable('obj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'propName');
|
|
|
|
$result = $this->invoke('isPropOfObj', $propFetch, 'obj');
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function testIsPropOfObjDifferentVar(): void
|
|
{
|
|
$objVar = new Expr\Variable('otherObj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'propName');
|
|
|
|
$result = $this->invoke('isPropOfObj', $propFetch, 'obj');
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function testIsPropOfObjNotPropertyFetch(): void
|
|
{
|
|
$var = new Expr\Variable('obj');
|
|
$result = $this->invoke('isPropOfObj', $var, 'obj');
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function testIsPropOfObjNotVariable(): void
|
|
{
|
|
$methodCall = new Expr\MethodCall(new Expr\Variable('obj'), 'method');
|
|
$propFetch = new Expr\PropertyFetch($methodCall, 'propName');
|
|
|
|
$result = $this->invoke('isPropOfObj', $propFetch, 'obj');
|
|
$this->assertFalse($result, 'Property fetch on method call result should not match');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaPropOptimizer: isObjectDefinition
|
|
// ========================================================================
|
|
|
|
public function testIsObjectDefinitionNew(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'obj');
|
|
$newExpr = new Expr\New_(new Node\Name('MyClass'));
|
|
$assign = new Expr\Assign(new Expr\Variable('obj'), $newExpr);
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('isObjectDefinition', $ssaVar);
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function testIsObjectDefinitionNull(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'obj');
|
|
$result = $this->invoke('isObjectDefinition', $ssaVar);
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
public function testIsObjectDefinitionNotAssign(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'obj');
|
|
$ssaVar->definition = new Stmt\Foreach_(
|
|
new Expr\Variable('arr'),
|
|
new Expr\Variable('value')
|
|
);
|
|
|
|
$result = $this->invoke('isObjectDefinition', $ssaVar);
|
|
$this->assertFalse($result, 'Foreach is not an object definition');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaPropOptimizer: isClassSafeForPropHoisting
|
|
// ========================================================================
|
|
|
|
public function testIsClassSafeForPropHoistingClean(): void
|
|
{
|
|
$classDef = new ClassDef('SafeClass', 0, 'App');
|
|
$this->setProperty('classes', ['app_safeclass' => $classDef]);
|
|
|
|
$result = $this->invoke('isClassSafeForPropHoisting', 'App\\SafeClass');
|
|
$this->assertTrue($result, 'Class without __get/__set should be safe');
|
|
}
|
|
|
|
public function testIsClassSafeForPropHoistingWithGet(): void
|
|
{
|
|
$classDef = new ClassDef('MagicClass', 0, 'App');
|
|
$classDef->methods = ['__get' => new MethodDef(0, '__get')];
|
|
$this->setProperty('classes', ['app_magicclass' => $classDef]);
|
|
|
|
$result = $this->invoke('isClassSafeForPropHoisting', 'App\\MagicClass');
|
|
$this->assertFalse($result, 'Class with __get should NOT be safe');
|
|
}
|
|
|
|
public function testIsClassSafeForPropHoistingWithSet(): void
|
|
{
|
|
$classDef = new ClassDef('MagicClass', 0, 'App');
|
|
$classDef->methods = ['__set' => new MethodDef(0, '__set')];
|
|
$this->setProperty('classes', ['app_magicclass' => $classDef]);
|
|
|
|
$result = $this->invoke('isClassSafeForPropHoisting', 'App\\MagicClass');
|
|
$this->assertFalse($result, 'Class with __set should NOT be safe');
|
|
}
|
|
|
|
public function testIsClassSafeForPropHoistingUnknown(): void
|
|
{
|
|
$this->setProperty('classes', []);
|
|
|
|
$result = $this->invoke('isClassSafeForPropHoisting', 'Unknown\\Class');
|
|
$this->assertFalse($result, 'Unknown class should NOT be safe');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaPropOptimizer: hasDangerousPropOps
|
|
// ========================================================================
|
|
|
|
public function testHasDangerousPropOpsUnset(): void
|
|
{
|
|
$objVar = new Expr\Variable('obj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
|
|
$unset = new Stmt\Unset_([$propFetch]);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop')
|
|
));
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$unset, $read]);
|
|
$this->assertFalse($result, 'unset($obj->prop) is blocked by the object handlers and cannot invalidate a hoisted reference');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsUnsetDifferentObj(): void
|
|
{
|
|
$objVar = new Expr\Variable('other');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
|
|
$unset = new Stmt\Unset_([$propFetch]);
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$unset]);
|
|
$this->assertFalse($result, 'unset on different object should not match');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsAssignRef(): void
|
|
{
|
|
$objVar = new Expr\Variable('obj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
|
|
$refVar = new Expr\Variable('ref');
|
|
$assignRef = new Expr\AssignRef($refVar, $propFetch);
|
|
$stmt = new Stmt\Expression($assignRef);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop')
|
|
));
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertTrue($result, '&$obj->prop before a later access should be detected');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsAssignRefToProperty(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop');
|
|
$assignRef = new Expr\AssignRef($propFetch, new Expr\Variable('ref'));
|
|
$stmt = new Stmt\Expression($assignRef);
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]);
|
|
$this->assertTrue($result, '$obj->prop =& $ref should be detected');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsByRefArg(): void
|
|
{
|
|
$objVar = new Expr\Variable('obj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
|
|
$arg = new Arg($propFetch, true); // byRef = true
|
|
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]);
|
|
$stmt = new Stmt\Expression($funcCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop')
|
|
));
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertTrue($result, 'func(&$obj->prop) before a later access should be detected');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsRefval(): void
|
|
{
|
|
$objVar = new Expr\Variable('obj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
|
|
$refvalArg = new Arg($propFetch);
|
|
$refvalCall = new Expr\FuncCall(new Node\Name('refval'), [$refvalArg]);
|
|
$arg = new Arg($refvalCall);
|
|
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]);
|
|
$stmt = new Stmt\Expression($funcCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop')
|
|
));
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertTrue($result, 'func(refval($obj->prop)) before a later access should be detected');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsClean(): void
|
|
{
|
|
$objVar = new Expr\Variable('obj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
|
|
$assign = new Expr\Assign($propFetch, new Scalar\LNumber(42));
|
|
$stmt = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]);
|
|
$this->assertFalse($result, 'Regular property assignment should be safe');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsNestedInIf(): void
|
|
{
|
|
$objVar = new Expr\Variable('obj');
|
|
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
|
|
$assignRef = new Expr\AssignRef(new Expr\Variable('ref'), $propFetch);
|
|
$ifStmt = new Stmt\If_(new Expr\ConstFetch(new Node\Name('true')), [
|
|
'stmts' => [new Stmt\Expression($assignRef)],
|
|
'elseifs' => [],
|
|
'else' => null,
|
|
]);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop')
|
|
));
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$ifStmt, $read]);
|
|
$this->assertTrue($result, '&$obj->prop inside if before a later access should be detected');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsNestedRefvalInAssignment(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop');
|
|
$refvalCall = new Expr\FuncCall(new Node\Name('refval'), [new Arg($propFetch)]);
|
|
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($refvalCall)]);
|
|
$stmt = new Stmt\Expression(new Expr\Assign(new Expr\Variable('result'), $funcCall));
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop')
|
|
));
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertTrue($result, 'refval($obj->prop) nested in an assignment RHS before a later access should be detected');
|
|
}
|
|
|
|
public function testHasDangerousPropOpsNestedByRefInReturn(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop');
|
|
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($propFetch, true)]);
|
|
$stmt = new Stmt\Return_($funcCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop')
|
|
));
|
|
|
|
$result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertTrue($result, 'By-ref property argument nested in return before a later access should be detected');
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsTracksPropertyNames(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'b');
|
|
$assignRef = new Expr\AssignRef(new Expr\Variable('ref'), $propFetch);
|
|
$stmt = new Stmt\Expression($assignRef);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'b')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame(['b' => true], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsDynamicPropertyWildcard(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), new Expr\Variable('prop'));
|
|
$write = new Stmt\Expression(new Expr\Assign($propFetch, new Scalar\LNumber(5)));
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$write, $read]);
|
|
$this->assertSame(['a' => true], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsObjectArgumentWildcard(): void
|
|
{
|
|
$funcCall = new Expr\FuncCall(new Node\Name('mutate'), [new Arg(new Expr\Variable('obj'))]);
|
|
$stmt = new Stmt\Expression($funcCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame(['a' => true], $result, 'Passing the object to dynamic code may turn a property slot into a reference');
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsObjectMethodReceiverWildcard(): void
|
|
{
|
|
$methodCall = new Expr\MethodCall(new Expr\Variable('obj'), 'mutate');
|
|
$stmt = new Stmt\Expression($methodCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame(['a' => true], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsPropertyArgumentDoesNotExposeObject(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'a');
|
|
$funcCall = new Expr\FuncCall(new Node\Name('mutate'), [new Arg($propFetch)]);
|
|
$stmt = new Stmt\Expression($funcCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame([], $result, 'Passing a property value by value does not expose the owning object');
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsInternalFunctionObjectArgumentIsSafe(): void
|
|
{
|
|
$funcCall = new Expr\FuncCall(new Node\Name('gettype'), [new Arg(new Expr\Variable('obj'))]);
|
|
$stmt = new Stmt\Expression($funcCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsInternalStaticMethodObjectArgumentIsSafe(): void
|
|
{
|
|
$staticCall = new Expr\StaticCall(
|
|
new Node\Name('DateTimeImmutable'),
|
|
'createFromMutable',
|
|
[new Arg(new Expr\Variable('obj'))]
|
|
);
|
|
$stmt = new Stmt\Expression($staticCall);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsEvalInvalidatesLaterPropertyAccess(): void
|
|
{
|
|
$eval = new Expr\Eval_(new Scalar\String_('$obj->a = 99;'));
|
|
$stmt = new Stmt\Expression($eval);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame(['a' => true], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsIncludeInvalidatesLaterPropertyAccess(): void
|
|
{
|
|
$include = new Expr\Include_(new Scalar\String_('unknown.php'), Expr\Include_::TYPE_INCLUDE);
|
|
$stmt = new Stmt\Expression($include);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame(['a' => true], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsObjectAliasWildcard(): void
|
|
{
|
|
$assign = new Expr\Assign(new Expr\Variable('alias'), new Expr\Variable('obj'));
|
|
$stmt = new Stmt\Expression($assign);
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
|
|
$this->assertSame(['a' => true], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsPropertyReadDoesNotExposeObject(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'a');
|
|
$assign = new Expr\Assign(new Expr\Variable('value'), $propFetch);
|
|
$stmt = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt]);
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsUnsetAfterLastAccessIsSafe(): void
|
|
{
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
$unset = new Stmt\Unset_([new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')]);
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$read, $unset]);
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsAssignRefToPropertyIsAlwaysUnsafe(): void
|
|
{
|
|
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'a');
|
|
$assignRef = new Expr\AssignRef($propFetch, new Expr\Variable('ref'));
|
|
$stmt = new Stmt\Expression($assignRef);
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt]);
|
|
$this->assertSame(['a' => true], $result);
|
|
}
|
|
|
|
public function testCollectDangerousPropOpsObjectArgumentAfterLastAccessIsSafe(): void
|
|
{
|
|
$read = new Stmt\Expression(new Expr\Assign(
|
|
new Expr\Variable('value'),
|
|
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
|
|
));
|
|
$funcCall = new Expr\FuncCall(new Node\Name('mutate'), [new Arg(new Expr\Variable('obj'))]);
|
|
$stmt = new Stmt\Expression($funcCall);
|
|
|
|
$result = $this->invoke('collectDangerousPropOps', 'obj', [$read, $stmt]);
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaPropOptimizer: isObjectSsaStable
|
|
// ========================================================================
|
|
|
|
public function testIsObjectSsaStableSingleDef(): void
|
|
{
|
|
$builder = new SsaBuilder([]);
|
|
$ssaVar = new SsaVar(0, 'obj');
|
|
$newExpr = new Expr\New_(new Node\Name('MyClass'));
|
|
$assign = new Expr\Assign(new Expr\Variable('obj'), $newExpr);
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
$builder->ssaVars[0] = $ssaVar;
|
|
|
|
$result = $this->invoke('isObjectSsaStable', $builder, 'obj');
|
|
$this->assertTrue($result, 'Single SSA def from new Expr should be stable');
|
|
}
|
|
|
|
public function testIsObjectSsaStableWithReference(): void
|
|
{
|
|
$builder = new SsaBuilder([]);
|
|
$ssaVar = new SsaVar(0, 'obj', SsaFlags::REFERENCE);
|
|
$builder->ssaVars[0] = $ssaVar;
|
|
|
|
$result = $this->invoke('isObjectSsaStable', $builder, 'obj');
|
|
$this->assertFalse($result, 'REFERENCE flag should make object unstable');
|
|
}
|
|
|
|
public function testIsObjectSsaStableWithEscaped(): void
|
|
{
|
|
$builder = new SsaBuilder([]);
|
|
$ssaVar = new SsaVar(0, 'obj', SsaFlags::ESCAPED);
|
|
$builder->ssaVars[0] = $ssaVar;
|
|
|
|
$result = $this->invoke('isObjectSsaStable', $builder, 'obj');
|
|
$this->assertFalse($result, 'ESCAPED flag should make object unstable');
|
|
}
|
|
|
|
public function testIsObjectSsaStableWithKilled(): void
|
|
{
|
|
$builder = new SsaBuilder([]);
|
|
$ssaVar = new SsaVar(0, 'obj', SsaFlags::KILLED);
|
|
$builder->ssaVars[0] = $ssaVar;
|
|
|
|
$result = $this->invoke('isObjectSsaStable', $builder, 'obj');
|
|
$this->assertFalse($result, 'KILLED flag should make object unstable');
|
|
}
|
|
|
|
public function testIsObjectSsaStableWithPhi(): void
|
|
{
|
|
$builder = new SsaBuilder([]);
|
|
$ssaVar = new SsaVar(0, 'obj', SsaFlags::PHI);
|
|
$builder->ssaVars[0] = $ssaVar;
|
|
|
|
$result = $this->invoke('isObjectSsaStable', $builder, 'obj');
|
|
$this->assertFalse($result, 'PHI should be skipped, no stable def found');
|
|
}
|
|
|
|
public function testIsObjectSsaStableNotFound(): void
|
|
{
|
|
$builder = new SsaBuilder([]);
|
|
$result = $this->invoke('isObjectSsaStable', $builder, 'nonexistent');
|
|
$this->assertFalse($result, 'Non-existent variable should not be stable');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaPropOptimizer: isStableObject (public method)
|
|
// ========================================================================
|
|
|
|
public function testIsStableObject(): void
|
|
{
|
|
$this->invoke('resetFunction');
|
|
$this->setContextProperty('stableObjects', ['myObj' => 'App\\MyClass']);
|
|
|
|
$this->assertTrue($this->compiler->isStableObject('myObj'));
|
|
$this->assertFalse($this->compiler->isStableObject('unknown'));
|
|
}
|
|
|
|
public function testCanHoistStableObjectPropAllowsCleanProperty(): void
|
|
{
|
|
$this->invoke('resetFunction');
|
|
$this->setContextProperty('stableObjects', ['obj' => 'App\\MyClass']);
|
|
$this->setContextProperty('unsafeObjectProps', ['obj' => ['b' => true]]);
|
|
|
|
$this->assertTrue($this->compiler->canHoistStableObjectProp('obj', 'a'));
|
|
$this->assertFalse($this->compiler->canHoistStableObjectProp('obj', 'b'));
|
|
}
|
|
|
|
public function testCanHoistStableObjectPropRejectsWildcard(): void
|
|
{
|
|
$this->invoke('resetFunction');
|
|
$this->setContextProperty('stableObjects', ['obj' => 'App\\MyClass']);
|
|
$this->setContextProperty('unsafeObjectProps', ['obj' => ['*' => true]]);
|
|
|
|
$this->assertFalse($this->compiler->canHoistStableObjectProp('obj', 'a'));
|
|
}
|
|
|
|
// ========================================================================
|
|
// LoopVarOptimizer: range-proven counters
|
|
// ========================================================================
|
|
|
|
public function testLoopVarOptimizerNarrowsWhilePostDecFromConstant(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
$n = 1000;
|
|
while ($n--) {
|
|
echo $n;
|
|
}
|
|
');
|
|
|
|
$this->assertSame(CompilerBase::TYPE_INT, $locals['n'] ?? null);
|
|
}
|
|
|
|
public function testLoopVarOptimizerNarrowsForCounterAndConstantBoundVar(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
$n = 20000;
|
|
for ($i = 0; $i < $n; $i++) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
|
|
$this->assertSame(CompilerBase::TYPE_INT, $locals['n'] ?? null);
|
|
}
|
|
|
|
public function testLoopVarOptimizerNarrowsForCounterWithStrlenBound(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
for ($i = 0; $i <= strlen($s); $i++) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
|
|
}
|
|
|
|
public function testLoopVarOptimizerNarrowsForCounterWithGenericIntFunctionBound(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
for ($i = 0; $i < time(); $i++) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
|
|
}
|
|
|
|
public function testLoopVarOptimizerRejectsInclusiveGenericIntFunctionBound(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
for ($i = 0; $i <= time(); $i++) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertArrayNotHasKey('i', $locals);
|
|
}
|
|
|
|
public function testLoopVarOptimizerNarrowsDescendingCounterFromIntFunction(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
for ($i = time(); $i > 0; $i--) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
|
|
}
|
|
|
|
public function testLoopVarOptimizerNarrowsDescendingCounterFromIntMethod(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
for ($i = $v->toInt(); $i > 0; --$i) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertSame(CompilerBase::TYPE_INT, $locals['i'] ?? null);
|
|
}
|
|
|
|
public function testLoopVarOptimizerRejectsBodyCounterMutation(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
$n = 10;
|
|
for ($i = 0; $i < $n; $i++) {
|
|
$i += 2;
|
|
}
|
|
');
|
|
|
|
$this->assertArrayNotHasKey('i', $locals);
|
|
}
|
|
|
|
public function testLoopVarOptimizerRejectsNonUnitStep(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
$n = 10;
|
|
for ($i = 0; $i < $n; $i += 2) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertArrayNotHasKey('i', $locals);
|
|
}
|
|
|
|
public function testLoopVarOptimizerRejectsUnsafeUseAfterLoop(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
$n = 10;
|
|
for ($i = 0; $i < $n; $i++) {
|
|
echo $i;
|
|
}
|
|
$x = $i + PHP_INT_MAX;
|
|
');
|
|
|
|
$this->assertArrayNotHasKey('i', $locals);
|
|
}
|
|
|
|
public function testLoopVarOptimizerRejectsCounterWhenBoundVarCanChangeBeforeLoop(): void
|
|
{
|
|
$locals = $this->optimizeLoopVarsForCode('
|
|
$n = 10;
|
|
if (time()) {
|
|
$n = "not-int";
|
|
}
|
|
for ($i = 0; $i < $n; $i++) {
|
|
echo $i;
|
|
}
|
|
');
|
|
|
|
$this->assertArrayNotHasKey('i', $locals);
|
|
$this->assertArrayNotHasKey('n', $locals);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaTypeOptimizer: detectSsaDefType
|
|
// ========================================================================
|
|
|
|
public function testDetectSsaDefTypeIntLiteral(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$assign = new Expr\Assign(new Expr\Variable('x'), new Scalar\LNumber(42));
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertEquals(CompilerBase::TYPE_INT, $result);
|
|
}
|
|
|
|
public function testDetectSsaDefTypeFloatLiteral(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$assign = new Expr\Assign(new Expr\Variable('x'), new Scalar\DNumber(3.14));
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertEquals(CompilerBase::TYPE_FLOAT, $result);
|
|
}
|
|
|
|
public function testDetectSsaDefTypeStringLiteral(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$assign = new Expr\Assign(new Expr\Variable('x'), new Scalar\String_('hello'));
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertEquals(CompilerBase::TYPE_STR, $result);
|
|
}
|
|
|
|
public function testDetectSsaDefTypeBoolLiteral(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$assign = new Expr\Assign(new Expr\Variable('x'), new Expr\ConstFetch(new Node\Name('true')));
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertEquals(CompilerBase::TYPE_BOOL, $result);
|
|
}
|
|
|
|
public function testDetectTypeOfExplicitStdNativeCalls(): void
|
|
{
|
|
$cases = [
|
|
'int' => CompilerBase::TYPE_INT,
|
|
'float' => CompilerBase::TYPE_FLOAT,
|
|
'bool' => CompilerBase::TYPE_BOOL,
|
|
];
|
|
|
|
foreach ($cases as $method => $expectedType) {
|
|
$call = new Expr\StaticCall(
|
|
new Node\Name('std'),
|
|
new Node\Identifier($method),
|
|
[new Arg(new Scalar\LNumber(1))]
|
|
);
|
|
|
|
$this->assertSame($expectedType, $this->invoke('detectTypeOfExpr', $call));
|
|
}
|
|
}
|
|
|
|
public function testDetectSsaDefTypeNull(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertNull($result, 'SSA var without definition should return null');
|
|
}
|
|
|
|
public function testDetectSsaDefTypeForeach(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$ssaVar->definition = new Stmt\Foreach_(
|
|
new Expr\Variable('arr'),
|
|
new Expr\Variable('x')
|
|
);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertNull($result, 'Foreach variable definition type is unknown');
|
|
}
|
|
|
|
public function testDetectSsaDefTypeAssignOpModIsUnknown(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$assignOp = new Expr\AssignOp\Mod(new Expr\Variable('x'), new Scalar\LNumber(10));
|
|
$ssaVar->definition = new Stmt\Expression($assignOp);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertNull($result, 'Compound assignment should not prove a narrow int type');
|
|
}
|
|
|
|
public function testDetectSsaDefTypeAssignOpDiv(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$assignOp = new Expr\AssignOp\Div(new Expr\Variable('x'), new Scalar\LNumber(2));
|
|
$ssaVar->definition = new Stmt\Expression($assignOp);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertEquals(CompilerBase::TYPE_FLOAT, $result);
|
|
}
|
|
|
|
public function testDetectSsaDefTypeAssignOpPlusIsUnknownForIntRhs(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$assignOp = new Expr\AssignOp\Plus(new Expr\Variable('x'), new Scalar\LNumber(1));
|
|
$ssaVar->definition = new Stmt\Expression($assignOp);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertNull($result, 'Int += can overflow at runtime, so SSA should not infer a narrow int type');
|
|
}
|
|
|
|
public function testDetectSsaDefTypeArithmeticIntExprIsUnknown(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$expr = new Expr\BinaryOp\Plus(new Scalar\LNumber(1), new Scalar\LNumber(2));
|
|
$assign = new Expr\Assign(new Expr\Variable('x'), $expr);
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertNull($result, 'Even constant + is not used for SSA int narrowing without a range-proven fold');
|
|
}
|
|
|
|
public function testDetectSsaDefTypeLiteralBitwiseIntExpr(): void
|
|
{
|
|
$ssaVar = new SsaVar(1, 'x');
|
|
$expr = new Expr\BinaryOp\BitwiseAnd(new Scalar\LNumber(7), new Scalar\LNumber(3));
|
|
$assign = new Expr\Assign(new Expr\Variable('x'), $expr);
|
|
$ssaVar->definition = new Stmt\Expression($assign);
|
|
|
|
$result = $this->invoke('detectSsaDefType', $ssaVar);
|
|
$this->assertEquals(CompilerBase::TYPE_INT, $result);
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaTypeOptimizer: exprCanOverflowInt
|
|
// ========================================================================
|
|
|
|
public function testExprCanOverflowIntDivision(): void
|
|
{
|
|
$div = new Expr\BinaryOp\Div(
|
|
new Scalar\LNumber(10),
|
|
new Scalar\LNumber(3)
|
|
);
|
|
$this->assertTrue($this->invoke('exprCanOverflowInt', $div), 'Division always can overflow');
|
|
}
|
|
|
|
public function testExprCanOverflowIntBitwiseSafe(): void
|
|
{
|
|
$and = new Expr\BinaryOp\BitwiseAnd(
|
|
new Scalar\LNumber(0xFF),
|
|
new Scalar\LNumber(0x0F)
|
|
);
|
|
$this->assertFalse($this->invoke('exprCanOverflowInt', $and), 'Bitwise AND is safe');
|
|
}
|
|
|
|
public function testExprCanOverflowIntConstantArithmetic(): void
|
|
{
|
|
$plus = new Expr\BinaryOp\Plus(
|
|
new Scalar\LNumber(1),
|
|
new Scalar\LNumber(2)
|
|
);
|
|
$this->assertTrue($this->invoke('exprCanOverflowInt', $plus), 'Arithmetic is treated as unsafe without range proof');
|
|
}
|
|
|
|
public function testExprCanOverflowIntPow(): void
|
|
{
|
|
$pow = new Expr\BinaryOp\Pow(
|
|
new Scalar\LNumber(2),
|
|
new Scalar\LNumber(63)
|
|
);
|
|
$this->assertTrue($this->invoke('exprCanOverflowInt', $pow), 'Pow can overflow');
|
|
}
|
|
|
|
public function testExprCanOverflowIntPhpIntMax(): void
|
|
{
|
|
$plus = new Expr\BinaryOp\Plus(
|
|
new Expr\ConstFetch(new Node\Name('PHP_INT_MAX')),
|
|
new Scalar\LNumber(1)
|
|
);
|
|
$this->assertTrue($this->invoke('exprCanOverflowInt', $plus), 'PHP_INT_MAX + 1 can overflow');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaTypeOptimizer: isBoundaryConstant
|
|
// ========================================================================
|
|
|
|
public function testIsBoundaryConstantPhpIntMax(): void
|
|
{
|
|
$const = new Expr\ConstFetch(new Node\Name('PHP_INT_MAX'));
|
|
$this->assertTrue($this->invoke('isBoundaryConstant', $const));
|
|
}
|
|
|
|
public function testIsBoundaryConstantPhpIntMin(): void
|
|
{
|
|
$const = new Expr\ConstFetch(new Node\Name('PHP_INT_MIN'));
|
|
$this->assertTrue($this->invoke('isBoundaryConstant', $const));
|
|
}
|
|
|
|
public function testIsBoundaryConstantOther(): void
|
|
{
|
|
$const = new Expr\ConstFetch(new Node\Name('FOO'));
|
|
$this->assertFalse($this->invoke('isBoundaryConstant', $const));
|
|
}
|
|
|
|
public function testIsBoundaryConstantNonConst(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$this->assertFalse($this->invoke('isBoundaryConstant', $var));
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaTypeOptimizer: hasDangerousIntOps
|
|
// ========================================================================
|
|
|
|
public function testHasDangerousIntOpsArithmeticPlus(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$plus = new Expr\BinaryOp\Plus($var, new Scalar\LNumber(1));
|
|
$stmt = new Stmt\Expression($plus);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'Arithmetic + with variable should be dangerous');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsBitwiseAnd(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$and = new Expr\BinaryOp\BitwiseAnd($var, new Scalar\LNumber(0xFF));
|
|
$stmt = new Stmt\Expression($and);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertFalse($result, 'Bitwise AND should be safe');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsComparison(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$comparison = new Expr\BinaryOp\Greater($var, new Scalar\LNumber(0));
|
|
$stmt = new Stmt\Expression($comparison);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertFalse($result, 'Comparison reads an int but cannot overflow it');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsAssignOpPlus(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$plusEq = new Expr\AssignOp\Plus($var, new Scalar\LNumber(1));
|
|
$stmt = new Stmt\Expression($plusEq);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'AssignOp\Plus should be dangerous');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsAssignOpBitwiseAnd(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$andEq = new Expr\AssignOp\BitwiseAnd($var, new Scalar\LNumber(0x0F));
|
|
$stmt = new Stmt\Expression($andEq);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'Compound assignment should be treated as dangerous for int narrowing');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsIncrement(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$postInc = new Expr\PostInc($var);
|
|
$stmt = new Stmt\Expression($postInc);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'PostInc should be dangerous');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsBitwiseNot(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$bitNot = new Expr\BitwiseNot($var);
|
|
$stmt = new Stmt\Expression($bitNot);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertFalse($result, 'BitwiseNot should be safe');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsUnaryMinus(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$unaryMinus = new Expr\UnaryMinus($var);
|
|
$stmt = new Stmt\Expression($unaryMinus);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'UnaryMinus should be dangerous');
|
|
}
|
|
|
|
public function testHasDangerousIntOpsDifferentVariable(): void
|
|
{
|
|
$y = new Expr\Variable('y');
|
|
$plus = new Expr\BinaryOp\Plus($y, new Scalar\LNumber(1));
|
|
$stmt = new Stmt\Expression($plus);
|
|
|
|
$result = $this->invoke('hasDangerousIntOps', 'x', [$stmt]);
|
|
$this->assertFalse($result, 'Operation on different variable should be safe for x');
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaTypeOptimizer: exprHasIntHazard
|
|
// ========================================================================
|
|
|
|
public function testExprHasIntHazardAdd(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$plus = new Expr\BinaryOp\Plus($var, new Scalar\LNumber(1));
|
|
|
|
$this->assertTrue($this->invoke('exprHasIntHazard', $plus, 'x'));
|
|
}
|
|
|
|
public function testExprHasIntHazardBitwiseAnd(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$and = new Expr\BinaryOp\BitwiseAnd($var, new Scalar\LNumber(0xFF));
|
|
|
|
$this->assertFalse($this->invoke('exprHasIntHazard', $and, 'x'));
|
|
}
|
|
|
|
public function testExprHasIntHazardBitwiseOr(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$or = new Expr\BinaryOp\BitwiseOr($var, new Scalar\LNumber(1));
|
|
|
|
$this->assertFalse($this->invoke('exprHasIntHazard', $or, 'x'));
|
|
}
|
|
|
|
public function testExprHasIntHazardMod(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$mod = new Expr\BinaryOp\Mod($var, new Scalar\LNumber(10));
|
|
|
|
$this->assertFalse($this->invoke('exprHasIntHazard', $mod, 'x'));
|
|
}
|
|
|
|
public function testExprHasIntHazardShiftLeft(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$shl = new Expr\BinaryOp\ShiftLeft($var, new Scalar\LNumber(2));
|
|
|
|
$this->assertFalse($this->invoke('exprHasIntHazard', $shl, 'x'));
|
|
}
|
|
|
|
public function testExprHasIntHazardCastInt(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$cast = new Expr\Cast\Int_($var);
|
|
|
|
$this->assertFalse($this->invoke('exprHasIntHazard', $cast, 'x'));
|
|
}
|
|
|
|
// ========================================================================
|
|
// SsaTypeOptimizer: hasDangerousFloatOps
|
|
// ========================================================================
|
|
|
|
public function testHasDangerousFloatOpsBitwiseAssign(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$andEq = new Expr\AssignOp\BitwiseAnd($var, new Scalar\LNumber(1));
|
|
$stmt = new Stmt\Expression($andEq);
|
|
|
|
$result = $this->invoke('hasDangerousFloatOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'Bitwise assign-op on float should be dangerous');
|
|
}
|
|
|
|
public function testHasDangerousFloatOpsNestedBitwiseBinary(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$bitwise = new Expr\BinaryOp\BitwiseAnd($var, new Scalar\LNumber(1));
|
|
$stmt = new Stmt\Expression(new Expr\Assign(new Expr\Variable('y'), $bitwise));
|
|
|
|
$result = $this->invoke('hasDangerousFloatOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'Bitwise binary op nested in an assignment should be dangerous for float');
|
|
}
|
|
|
|
public function testHasDangerousFloatOpsReturnMod(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$mod = new Expr\BinaryOp\Mod($var, new Scalar\LNumber(2));
|
|
$stmt = new Stmt\Return_($mod);
|
|
|
|
$result = $this->invoke('hasDangerousFloatOps', 'x', [$stmt]);
|
|
$this->assertTrue($result, 'Modulo in return should be dangerous for float');
|
|
}
|
|
|
|
public function testHasDangerousFloatOpsArithmetic(): void
|
|
{
|
|
$var = new Expr\Variable('x');
|
|
$plusEq = new Expr\AssignOp\Plus($var, new Scalar\DNumber(1.5));
|
|
$stmt = new Stmt\Expression($plusEq);
|
|
|
|
$result = $this->invoke('hasDangerousFloatOps', 'x', [$stmt]);
|
|
$this->assertFalse($result, 'Arithmetic assign-op on float should be safe');
|
|
}
|
|
}
|
|
|